Merge #14433 input methods and remove Rime

The keyboard choice and omarchy-setup-input own Chinese input now. The
new passwordless sudo and input tooltips get Chinese text.
This commit is contained in:
ZacharyZhang-NY committed 2026-10-08 23:30:41 -04:00
commit fdd457c0fa
86 files changed
+2545 -207

No files matched your search

+39
View File
@@ -0,0 +1,39 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/bin" "$test_tmp/home/.local/share/applications"
for helper in omarchy-pkg-add omarchy-pkg-drop update-desktop-database; do
cat > "$test_tmp/bin/$helper" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >> "$TEST_LOG"
SH
chmod +x "$test_tmp/bin/$helper"
done
launcher="$test_tmp/home/.local/share/applications/Disk Usage.desktop"
printf '[Desktop Entry]\nExec=dua i /\n' > "$launcher"
for (( attempt = 0; attempt < 2; attempt++ )); do
env HOME="$test_tmp/home" OMARCHY_PATH="$ROOT" TEST_LOG="$test_tmp/log" PATH="$test_tmp/bin:$PATH" \
bash -euo pipefail "$ROOT/migrations/1791345484.sh"
[[ ! -e $launcher ]] || fail "the migration removes the old Disk Usage launcher"
done
pass "the Disktree migration removes the old launcher and can run twice"
expected="omarchy-pkg-add disktree-bin
omarchy-pkg-drop dua-cli
update-desktop-database $test_tmp/home/.local/share/applications"
[[ $(cat "$test_tmp/log") == "$expected"$'\n'"$expected" ]] || fail "the migration installs Disktree before removing dua and refreshing launchers"
pass "the migration installs Disktree before removing dua and refreshing launchers"
rm -r "$test_tmp/home/.local/share/applications"
env HOME="$test_tmp/home" OMARCHY_PATH="$ROOT" TEST_LOG="$test_tmp/log" PATH="$test_tmp/bin:$PATH" \
bash -euo pipefail "$ROOT/migrations/1791345484.sh"
[[ ! -d $test_tmp/home/.local/share/applications ]] || fail "the migration relies on the packaged Disktree launcher"
pass "the migration relies on the packaged Disktree launcher when no local launchers exist"
@@ -52,7 +52,9 @@ case "$name" in
;;
mv)
[[ ${TEST_PUBLISH_FAIL:-0} != 1 ]] || exit 1
[[ ${TEST_REQUIRE_EXISTING_TARGET:-0} != 1 || -f ${@: -1} ]] || exit 1
/usr/bin/mv "$@"
[[ ${TEST_KILL_AFTER_PUBLISH:-0} != 1 ]] || /usr/bin/kill -KILL "$PPID"
[[ ${TEST_POST_PUBLISH_FAIL:-0} != 1 ]] || : >"$TEST_GRANT_ROOT/run/omarchy-sudo-passwordless-package-removing"
;;
systemd-run)
@@ -69,7 +71,15 @@ case "$name" in
sudo)
if [[ ${1:-} == -h ]]; then echo 'usage: sudo [-N] command'; exit 0; fi
if [[ ${1:-} == -k ]]; then exit 0; fi
if [[ ${1:-} == -N ]]; then shift; fi
if [[ ${1:-} == -n && ${3:-} == -l ]]; then
[[ ${TEST_POLICY_FAILURE:-0} != 1 ]] || exit 1
default_policy=' Options: authenticate'
[[ ${TEST_STATUS:-3} == 3 ]] || default_policy=' Options: !authenticate'
printf '%s\n' "${TEST_POLICY:-$default_policy}"
exit 0
fi
if [[ ${1:-} == -n ]]; then shift; fi
if [[ ${1:-} == -N || ${1:-} == -kn ]]; then shift; fi
if [[ ${1:-} == -- ]]; then shift; fi
if [[ ${TEST_MIGRATION:-0} == 1 ]]; then
[[ ${TEST_NO_SUDO:-0} != 1 ]] || exit 1
@@ -78,7 +88,14 @@ case "$name" in
[[ ${2:-} != __status ]] || exit "${TEST_STATUS:-3}"
fi
;;
gum) exit 1 ;;
gum)
if [[ $1 == choose ]]; then
[[ ${TEST_CHOICE_CANCEL:-0} != 1 ]] || exit 130
printf '%s\n' "${TEST_CHOICE:-15 minutes}"
else
exit "${TEST_CONFIRM_STATUS:-1}"
fi
;;
*) exit 99 ;;
esac
STUB
@@ -120,6 +137,6 @@ assert_status() {
(( actual == expected )) || fail "expected status $expected, got $actual from $*"
}
reset_grant() {
rm -f "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000" "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
rm -f "$test_tmp/etc/sudoers.d/99-omarchy-permanent-nopasswd-1000" "$test_tmp/etc/sudoers.d/99-omarchy-nopasswd-1000" "$test_tmp/run/omarchy-sudo-passwordless-package-removing"
: >"$test_tmp/commands"
}
+128
View File
@@ -0,0 +1,128 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
require_command lua
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
# Load the JIS bindings on a jp layout and fire one of them in a fake window,
# printing what it hands to Hyprland. TEST_FCITX5_CONFIG is the fcitx5 config,
# or unset for none.
fire() {
local keys="$1" class="$2" tags="${3-}"
local home="$test_dir/home"
rm -rf "$home"
mkdir -p "$home/.config/fcitx5"
if [[ -v TEST_FCITX5_CONFIG ]]; then
printf '%s\n' "$TEST_FCITX5_CONFIG" >"$home/.config/fcitx5/config"
fi
if [[ ${TEST_KEYBOARD_ONLY:-false} == "true" ]]; then
printf '[Groups/0/Items/0]\nName=keyboard-us\n' > "$home/.config/fcitx5/profile"
else
printf '[Groups/0/Items/0]\nName=keyboard-us\n[Groups/0/Items/1]\nName=mozc\n' > "$home/.config/fcitx5/profile"
fi
HOME="$home" XDG_CONFIG_HOME="$home/.config" OMARCHY_PATH="$ROOT" \
TEST_KEYS="$keys" TEST_CLASS="$class" TEST_TAGS="$tags" TEST_LAYOUT="${TEST_LAYOUT:-jp}" lua <<'LUA'
package.path = os.getenv("OMARCHY_PATH") .. "/?.lua;" .. package.path
local real_open = io.open
io.open = function(path, mode)
if path == "/etc/vconsole.conf" then
local file = io.tmpfile()
file:write("XKBLAYOUT=" .. os.getenv("TEST_LAYOUT") .. "\n")
file:seek("set")
return file
end
return real_open(path, mode)
end
local binds = {}
o = {
bind = function(keys, description, dispatcher, options)
binds[keys] = { dispatcher = dispatcher, options = options or {} }
end,
}
local window = { class = os.getenv("TEST_CLASS"), tags = {} }
for tag in os.getenv("TEST_TAGS"):gmatch("[^,]+") do
table.insert(window.tags, tag)
end
hl = {
dsp = {
send_key_state = function(key)
return ("send %s %s %s"):format(key.mods, key.key, key.state)
end,
},
dispatch = function(action) print(action) end,
timer = function(callback) callback() end,
get_active_window = function() return window end,
exec_cmd = function(command) print("exec " .. command) end,
}
o.shell_succeeds = function(command) print("exec " .. command) end
require("default.hypr.bindings.input-method")
require("default.hypr.bindings.jis")
local bind = binds[os.getenv("TEST_KEYS")]
if bind.options.non_consuming then
print("non_consuming")
end
bind.dispatcher()
LUA
}
assert_fires() {
local description="$1" expected="$2"
shift 2
local actual
actual=$(fire "$@")
[[ $actual == "$expected" ]] ||
fail "$description" "expected: $expected"$'\n'"actual: $actual"
pass "$description"
}
zoom="CTRL + SHIFT + semicolon"
assert_fires "the JIS + key zooms in with Ctrl + keypad plus" \
$'send CTRL KP_Add down\nsend CTRL KP_Add up' "$zoom" "chromium"
assert_fires "the JIS + key zooms in windows without a class" \
$'send CTRL KP_Add down\nsend CTRL KP_Add up' "$zoom" ""
assert_fires "the JIS + key zooms Obsidian in with Ctrl + ^" \
$'send CTRL asciicircum down\nsend CTRL asciicircum up' "$zoom" "obsidian"
assert_fires "the JIS + key zooms the Obsidian Flatpak in with Ctrl + ^" \
$'send CTRL asciicircum down\nsend CTRL asciicircum up' "$zoom" "md.obsidian.Obsidian"
# The prefix bind has to pass Ctrl + Space on, or tmux and Herdr never see it.
prefix="CTRL + SPACE"
freed=$'[Hotkey/TriggerKeys]\n0=Zenkaku_Hankaku\n\n[Hotkey/ActivateKeys]\n0=Henkan'
TEST_FCITX5_CONFIG=$freed assert_fires "the terminal prefix switches to direct input once Ctrl + Space no longer toggles" \
$'non_consuming\nexec timeout 0.3 fcitx5-remote --check -c' "$prefix" "com.mitchellh.ghostty" "terminal*"
TEST_FCITX5_CONFIG=$freed assert_fires "the prefix leaves input alone outside terminals" \
"non_consuming" "$prefix" "chromium" "browser"
# While fcitx5 still toggles on Ctrl + Space, switching to direct input would
# undo the toggle it just made.
assert_fires "the prefix leaves input alone without an fcitx5 config" \
"non_consuming" "$prefix" "com.mitchellh.ghostty" "terminal*"
TEST_FCITX5_CONFIG=$'[Behavior]\nShareInputState=No' assert_fires "the prefix leaves input alone with the default trigger keys" \
"non_consuming" "$prefix" "com.mitchellh.ghostty" "terminal*"
TEST_FCITX5_CONFIG=$'[Hotkey/TriggerKeys]\n0=Zenkaku_Hankaku\n1=Control+space' assert_fires "the prefix leaves input alone while Ctrl + Space is a trigger key" \
"non_consuming" "$prefix" "com.mitchellh.ghostty" "terminal*"
for layout in us kr; do
TEST_LAYOUT="$layout" TEST_FCITX5_CONFIG=$freed assert_fires "the terminal prefix works with $layout keyboards too" \
$'non_consuming\nexec timeout 0.3 fcitx5-remote --check -c' "$prefix" "foot" "terminal"
done
TEST_KEYBOARD_ONLY=true TEST_FCITX5_CONFIG=$freed assert_fires "keyboard-only profiles skip the terminal controller call" \
"non_consuming" "$prefix" "foot" "terminal"
@@ -13,6 +13,15 @@ local vconsole = os.getenv("OMARCHY_VCONSOLE")
local real_open = io.open
io.open = function(path, mode)
if path == "/etc/omarchy/input-method" then
local preference = os.getenv("TEST_INPUT_PREFERENCE")
if not preference then return nil end
local file = io.tmpfile()
file:write(preference)
file:seek("set")
return file
end
if path ~= "/etc/vconsole.conf" then
return real_open(path, mode)
end
@@ -128,3 +137,7 @@ sddm_layouts=$(sed -n '/^local non_latin_layouts =/,+1p' "$sddm_lua" | grep -o '
[[ $hooks_layouts == "$sddm_layouts" ]] ||
fail "greeter non-latin layout list stays in sync" "$(diff <(echo "$hooks_layouts") <(echo "$sddm_layouts"))"
pass "greeter non-latin layout list stays in sync with the initramfs hook"
TEST_INPUT_PREFERENCE=$'INPUT_METHOD=hangul\n XKB_LAYOUT="kr" # desktop only\n' assert_input "Korean selection overrides its installer US layout" "[kr] [] [$base_options]" $'XKBLAYOUT=us\n'
TEST_INPUT_PREFERENCE=$'INPUT_METHOD=hangul\nXKB_LAYOUT=kr\n' assert_input "a later US variant overrides the Korean preference" "[us] [intl] [$base_options]" $'XKBLAYOUT=us\nXKBVARIANT=intl\n'
TEST_INPUT_PREFERENCE=$'INPUT_METHOD=hangul\nXKB_LAYOUT=kr\n' assert_input "a later layout overrides the Korean preference" "[de] [nodeadkeys] [$base_options]" $'XKBLAYOUT=de\nXKBVARIANT=nodeadkeys\n'
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
OMARCHY_PATH="$ROOT" /usr/bin/python - <<'PY'
import importlib.util
import os
from unittest.mock import patch
spec = importlib.util.spec_from_file_location("indicator", os.environ["OMARCHY_PATH"] + "/default/input-methods/indicator.py")
indicator = importlib.util.module_from_spec(spec)
spec.loader.exec_module(indicator)
with patch.object(indicator, "call", side_effect=[
("Default",),
("us", [("keyboard-us", ""), ("mozc", ""), ("mozc", "")]),
("mozc", "Mozc", "", "fcitx_mozc", "あ", "ja", "mozc", True, "", {}),
]):
state = indicator.snapshot(None)
assert state == {"methods": ["keyboard-us", "mozc"], "current": "mozc", "name": "Mozc", "label": "あ", "language": "ja"}
print("ok - live state deduplicates modes and reads language independently of the engine label")
def check_cycle(current, methods, expected):
state = {"current": current, "methods": methods}
with patch.object(indicator, "snapshot", return_value=state), patch.object(indicator, "call") as call:
indicator.cycle(None)
if expected is None:
call.assert_not_called()
else:
assert call.call_args.args[1] == "SetCurrentIM"
assert call.call_args.args[2].unpack() == (expected,)
check_cycle("keyboard-us", ["keyboard-us", "mozc"], "mozc")
check_cycle("mozc", ["keyboard-us", "mozc"], "keyboard-us")
check_cycle("mozc", ["keyboard-us", "mozc", "hangul"], "hangul")
check_cycle("hangul", ["keyboard-us", "mozc", "hangul"], "keyboard-us")
check_cycle("keyboard-us", ["keyboard-us"], None)
check_cycle("removed", ["keyboard-us", "mozc"], None)
check_cycle("", [], None)
print("ok - input cycling covers Latin, multiple engines, single modes, and stale contexts")
entries = [("keyboard-us", "English", "", "input-keyboard", "en", "en", True),
("pinyin", "Pinyin", "", "fcitx-pinyin", "拼", "zh_CN", True)]
live = {"methods": ["keyboard-us", "pinyin"], "current": "", "name": "", "label": "", "language": ""}
selected = []
def fake_call(bus, method, args=None):
if method == "AvailableInputMethods":
return (entries,)
if method == "SetCurrentIM":
selected.append(args.unpack()[0])
live["current"] = selected[-1]
return ()
raise AssertionError(method)
with patch.object(indicator, "snapshot", side_effect=lambda bus: dict(live)), patch.object(indicator, "call", side_effect=fake_call):
reader = indicator.Indicator(None)
assert reader.refresh()["current"] == "keyboard-us"
reader.select_next()
assert reader.refresh()["current"] == "pinyin"
assert reader.pending == "pinyin" and selected == []
reader.select_next()
assert reader.pending == "keyboard-us"
reader.select_next()
live["current"] = "keyboard-us"
assert reader.refresh()["current"] == "pinyin"
assert reader.pending == "" and selected == ["pinyin"]
live["current"] = ""
reader.select_next()
live["methods"] = ["pinyin"]
reader.refresh()
assert reader.pending == ""
print("ok - desktop clicks update the label, cycle pending choices, and apply once a text context exists")
PY
+277
View File
@@ -0,0 +1,277 @@
import argparse
import importlib.util
import json
import os
from pathlib import Path
import subprocess
import tempfile
import unittest
from unittest.mock import patch
ROOT = Path(os.environ["OMARCHY_PATH"])
spec = importlib.util.spec_from_file_location("input_setup", ROOT / "default/input-methods/configure.py")
setup = importlib.util.module_from_spec(spec)
spec.loader.exec_module(setup)
class InputMethodTest(unittest.TestCase):
def test_pinyin_defaults_suppress_cloud_prompt(self):
setup.defaults(self.config, fresh=True)
pinyin = setup.values((self.config / "fcitx5/conf/pinyin.conf").read_text())
self.assertEqual(pinyin["FirstRun"], "False")
self.assertEqual(pinyin["CloudPinyinEnabled"], "False")
def test_pinyin_prompt_suppression_preserves_existing_preferences(self):
pinyin = self.config / "fcitx5/conf/pinyin.conf"
pinyin.parent.mkdir(parents=True)
pinyin.write_text("FirstRun=True\nCloudPinyinEnabled=True\nPageSize=7\n")
setup.defaults(self.config)
self.assertEqual(pinyin.read_text(), "FirstRun=False\nCloudPinyinEnabled=True\nPageSize=7\n")
def setUp(self):
self.temporary = tempfile.TemporaryDirectory()
self.addCleanup(self.temporary.cleanup)
self.home = Path(self.temporary.name)
self.config = self.home / "config"
self.vconsole = self.home / "vconsole"
self.preference = self.home / "preference"
self.vconsole.write_text("XKBLAYOUT=us\n")
self.preference.write_text("INPUT_METHOD=none\n")
env = {"XDG_CONFIG_HOME": str(self.config), "XDG_DATA_HOME": str(self.home / "data"), "OMARCHY_VCONSOLE": str(self.vconsole), "OMARCHY_INPUT_SELECTION": str(self.preference)}
self.environment = patch.dict(os.environ, env)
self.environment.start()
self.addCleanup(self.environment.stop)
self.process = patch.object(setup.subprocess, "run", return_value=subprocess.CompletedProcess([], 1, stdout="", stderr=""))
self.process.start()
self.addCleanup(self.process.stop)
def seed(self, method=None):
setup.configure(argparse.Namespace(method=method, seed=True, defaults=False))
def test_every_installer_engine_is_seeded_and_inactive(self):
for method in ("mozc", "hangul", "pinyin", "chewing"):
with self.subTest(method=method):
profile = self.config / "fcitx5/profile"
profile.unlink(missing_ok=True)
self.preference.write_text(f"INPUT_METHOD={method}\nXKB_LAYOUT={'kr' if method == 'hangul' else ''}\n")
self.seed()
data = setup.sections(profile.read_text())
self.assertEqual(data["Groups/0"]["DefaultIM"], method)
self.assertEqual(data["Groups/0/Items/1"]["Name"], method)
self.assertEqual(data["Groups/0/Items/0"]["Name"], "keyboard-kr" if method == "hangul" else "keyboard-us")
keys = setup.sections((self.config / "fcitx5/config").read_text())
self.assertEqual(keys["Behavior"]["ActiveByDefault"], "False")
self.assertNotIn("Control+space", keys["Hotkey/TriggerKeys"].values())
self.assertIn("Control+Shift+space", keys["Hotkey/TriggerKeys"].values())
def test_vconsole_variants_quotes_and_japanese_without_preference(self):
self.preference.unlink()
self.vconsole.write_text('XKBLAYOUT="jp" # JIS\n')
self.assertEqual(setup.selection(), ("mozc", "jp"))
self.vconsole.write_text("XKBLAYOUT='de'\nXKBVARIANT=nodeadkeys\n")
self.seed()
self.assertIn("Default Layout=de-nodeadkeys", (self.config / "fcitx5/profile").read_text())
self.vconsole.write_text("XKBLAYOUT=ru\nXKBVARIANT=phonetic\n")
self.assertEqual(setup.selection(), ("none", "us"))
def test_rerun_preserves_custom_multilingual_profile(self):
self.seed("mozc")
profile = self.config / "fcitx5/profile"
original = profile.read_bytes() + b"\n# custom configuration\n"
profile.write_bytes(original)
self.seed("hangul")
self.assertEqual(profile.read_bytes(), original)
def test_stock_profile_is_repaired_but_custom_single_keyboard_is_not(self):
self.seed()
self.vconsole.write_text("XKBLAYOUT=fr\n")
self.seed()
profile = self.config / "fcitx5/profile"
self.assertIn("keyboard-fr", profile.read_text())
self.seed("mozc")
self.assertNotIn("Name=mozc", profile.read_text())
def test_existing_shortcuts_and_addon_options_are_preserved(self):
path = self.config / "fcitx5/config"
setup.atomic_write(path, "# user's keys\n[Hotkey/TriggerKeys]\n0=Alt+space\n\n[Behavior]\nActiveByDefault=True\n")
quickphrase = self.config / "fcitx5/conf/quickphrase.conf"
setup.atomic_write(quickphrase, "[TriggerKey]\n\n")
setup.defaults(self.config, fresh=True)
config = setup.sections(path.read_text())
self.assertEqual(config["Hotkey/TriggerKeys"], {"0": "Alt+space"})
self.assertEqual(config["Behavior"], {"ActiveByDefault": "True"})
self.assertEqual(quickphrase.read_text(), "[TriggerKey]\n\n")
def test_generated_stock_shortcuts_are_repaired(self):
path = self.config / "fcitx5/config"
setup.atomic_write(path, "[Hotkey/TriggerKeys]\n0=Control+space\n1=Zenkaku_Hankaku\n2=Hangul\n\n[Hotkey/EnumerateGroupForwardKeys]\n0=Super+space\n")
setup.defaults(self.config, fresh=True)
config = setup.sections(path.read_text())
self.assertEqual(config["Hotkey/EnumerateGroupForwardKeys"], {})
self.assertNotIn("Control+space", config["Hotkey/TriggerKeys"].values())
def test_existing_engine_inherited_hotkeys_are_preserved(self):
path = self.config / "fcitx5/config"
setup.atomic_write(path, "[Behavior]\nActiveByDefault=True\n")
setup.defaults(self.config, fresh=False)
self.assertEqual(path.read_text(), "[Behavior]\nActiveByDefault=True\n")
def test_korean_preference_yields_to_a_later_keyboard_change(self):
self.preference.write_text("INPUT_METHOD=hangul\nXKB_LAYOUT=kr\n")
self.assertEqual(setup.selection(), ("hangul", "kr"))
for layout, variant, expected in [("de", "nodeadkeys", "de-nodeadkeys"), ("us", "intl", "us-intl")]:
self.vconsole.write_text(f"XKBLAYOUT={layout}\nXKBVARIANT={variant}\n")
self.assertEqual(setup.selection(), ("hangul", expected))
def test_live_append_keeps_every_method_and_layout(self):
info = json.dumps({"data": ["de-nodeadkeys", [["keyboard-de-nodeadkeys", ""], ["mozc", "jp"]]]})
updated = json.dumps({"data": ["de-nodeadkeys", [["keyboard-de-nodeadkeys", ""], ["mozc", "jp"], ["hangul", ""]]]})
with patch.object(setup, "run", side_effect=["Work", info, "", "", "Work", updated]) as calls:
setup.live_append("hangul")
self.assertEqual(calls.call_args_list[2].args[0][-10:], ["ssa(ss)", "Work", "de-nodeadkeys", "3", "keyboard-de-nodeadkeys", "", "mozc", "jp", "hangul", ""])
self.assertEqual(calls.call_args_list[3].args[0][-1], "Save")
info = json.dumps({"data": ["us", [["keyboard-us", ""], ["hangul", ""]]]})
with patch.object(setup, "run", side_effect=["Default", info]) as calls:
setup.live_append("hangul")
self.assertEqual(calls.call_count, 2)
def test_seed_in_a_live_desktop_is_a_harmless_retry(self):
self.seed()
profile = self.config / "fcitx5/profile"
original = profile.read_bytes()
with patch.object(setup.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)):
self.seed("mozc")
self.assertEqual(profile.read_bytes(), original)
def test_failed_seed_rolls_back_so_it_can_be_retried(self):
with patch.object(setup, "defaults", side_effect=OSError("disk full")):
with self.assertRaises(OSError):
self.seed("mozc")
self.assertFalse((self.config / "fcitx5/profile").exists())
self.seed("mozc")
self.assertIn("Name=mozc", (self.config / "fcitx5/profile").read_text())
def test_failed_live_setup_restores_disk_and_restarts_the_daemon(self):
self.seed()
profile = self.config / "fcitx5/profile"
original = profile.read_bytes()
events = []
def append(method):
profile.write_text("failed in-memory state")
raise RuntimeError("save failed")
def command(args):
events.append(args)
if args == ["omarchy-restart-xcompose"] and len(events) > 2:
self.assertEqual(profile.read_bytes(), original)
return ""
probes = 0
def process(args, **kwargs):
nonlocal probes
if args[0] == "pgrep":
probes += 1
return subprocess.CompletedProcess([], 0 if probes == 1 else 1)
return subprocess.CompletedProcess([], 0)
with patch.object(setup.subprocess, "run", side_effect=process), patch.object(setup, "run", side_effect=command), patch.object(setup, "wait_ready"), patch.object(setup, "live_append", side_effect=append), patch.object(setup.time, "sleep"):
with self.assertRaisesRegex(RuntimeError, "save failed"):
setup.configure(argparse.Namespace(method="mozc", seed=False, defaults=False))
self.assertEqual(profile.read_bytes(), original)
self.assertIn(["systemctl", "--user", "stop", "omarchy-fcitx5.service"], events)
self.assertEqual(events.count(["omarchy-restart-xcompose"]), 2)
def test_readiness_checks_the_dbus_array_payload_and_group(self):
available = json.dumps({"data": [[["mozc", "Mozc", "", "", "", "ja", False]]]})
with patch.object(setup, "run", return_value=available), patch.object(setup, "live_group", return_value=("Default", "us", [["keyboard-us", ""]])):
setup.wait_ready("mozc")
def test_a_silently_dropped_engine_is_rejected_and_group_restored(self):
old = ("Default", "us", [["keyboard-us", ""]])
with patch.object(setup, "live_group", side_effect=[old, old]), patch.object(setup, "live_set") as setter:
with self.assertRaisesRegex(RuntimeError, "retain"):
setup.live_append("mozc")
self.assertEqual(setter.call_args_list[-1].args, old)
def test_migration_without_a_bus_keeps_written_activation_and_defaults(self):
self.seed()
with patch.object(setup, "run", side_effect=subprocess.CalledProcessError(1, "busctl")):
setup.configure(argparse.Namespace(method=None, seed=False, defaults=True))
self.assertTrue((self.home / "data/dbus-1/services/org.fcitx.Fcitx5.service").exists())
def test_live_migration_without_a_bus_does_not_restart_or_rewrite_profile(self):
self.seed()
profile = self.config / "fcitx5/profile"
original = profile.read_bytes()
with patch.object(setup.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)), patch.object(setup, "run", side_effect=subprocess.CalledProcessError(1, "busctl")) as calls:
setup.configure(argparse.Namespace(method=None, seed=False, defaults=True))
self.assertEqual(profile.read_bytes(), original)
self.assertFalse(any(call.args[0] == ["omarchy-restart-xcompose"] for call in calls.call_args_list))
def test_migration_does_not_add_an_engine_the_old_daemon_cannot_load(self):
self.seed()
self.vconsole.write_text("XKBLAYOUT=jp\n")
self.preference.write_text("INPUT_METHOD=mozc\n")
old = ("Default", "us", [["keyboard-us", ""]])
new = ("Default", "jp", [["keyboard-jp", ""]])
with patch.object(setup.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)), patch.object(setup, "live_group", side_effect=[old, new]), patch.object(setup, "available_methods", return_value=[]), patch.object(setup, "live_set") as setter:
setup.configure(argparse.Namespace(method=None, seed=False, defaults=True))
self.assertEqual(setter.call_args.args, new)
def test_custom_keyboard_overrides_are_not_stock(self):
self.assertFalse(setup.stock_profile(setup.profile_text("de", "none").replace("keyboard-de", "keyboard-us")))
self.assertFalse(setup.stock_profile(setup.profile_text("us", "none").replace("Layout=\n", "Layout=ru\n")))
def test_selected_live_engine_is_verified_after_the_context_reconnects(self):
self.seed("mozc")
profile = self.config / "fcitx5/profile"
saves = 0
def command(args):
nonlocal saves
if args == setup.CONTROLLER + ["Save"]:
saves += 1
if saves == 2:
profile.write_text(profile.read_text().replace("DefaultIM=mozc", "DefaultIM=hangul"))
return "Default" if args == ["fcitx5-remote", "-q"] else ""
with patch.object(setup, "run", side_effect=command) as commands, patch.object(setup.time, "sleep"):
setup.select_live_method("hangul", profile)
self.assertEqual(saves, 2)
self.assertEqual(sum(call.args[0] == ["fcitx5-remote", "-c"] for call in commands.call_args_list), 2)
def test_live_setup_does_not_validate_an_unused_install_layout(self):
self.seed("mozc")
self.vconsole.write_text("XKBLAYOUT=invalid value\n")
with patch.object(setup.subprocess, "run", return_value=subprocess.CompletedProcess([], 0)), patch.object(setup, "run", return_value=""), patch.object(setup, "wait_ready"), patch.object(setup, "live_append"), patch.object(setup, "select_live_method"):
setup.configure(argparse.Namespace(method="hangul", seed=False, defaults=False))
def test_live_configuration_requires_a_desktop(self):
with self.assertRaisesRegex(RuntimeError, "Log in"):
setup.configure(argparse.Namespace(method="mozc", seed=False, defaults=False))
self.assertFalse((self.config / "fcitx5/profile").exists())
def test_atomic_updates_keep_symlinks_and_modes(self):
target = self.home / "real-config"
target.write_text("before")
target.chmod(0o600)
link = self.home / "link"
link.symlink_to(target)
setup.atomic_write(link, "after")
self.assertTrue(link.is_symlink())
self.assertEqual(target.read_text(), "after")
self.assertEqual(target.stat().st_mode & 0o777, 0o600)
def test_input_preferences_are_data_and_invalid_values_fail(self):
self.preference.write_text("INPUT_METHOD=$(touch /tmp/should-not-exist)\n")
with self.assertRaises(ValueError):
self.seed()
def test_font_fallback_does_not_replace_existing_preference(self):
setup.font_default(self.config, "mozc")
font = self.config / "fontconfig/conf.d/50-omarchy-input-method.conf"
self.assertIn("Noto Sans CJK JP", font.read_text())
self.assertIn("Noto Sans Mono CJK JP", font.read_text())
original = font.read_bytes()
setup.font_default(self.config, "hangul")
self.assertEqual(font.read_bytes(), original)
if __name__ == "__main__":
unittest.main()
+6
View File
@@ -0,0 +1,6 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
OMARCHY_PATH="$ROOT" python "$ROOT/test/shell.d/input-method-checks.py"
+3
View File
@@ -64,6 +64,9 @@ rendered=$(keybindings)
grep -q 'SUPER + F *→ Full screen' <<<"$rendered" ||
fail "a chord with no alternative renders on its own" "$rendered"
pass "the keybindings menu renders its entries"
grep -q 'CTRL SHIFT + SPACE *→ Switch input language' <<<"$rendered" ||
fail "the input switch shortcut appears in the cheatsheet" "$rendered"
pass "the input switch shortcut appears in the cheatsheet"
(( $(grep -c '→ Close window$' <<<"$rendered") == 1 )) ||
fail "an alternative chord joins the row of the first one" "$rendered"
+17
View File
@@ -7,6 +7,23 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const model = requireFromRoot('shell/plugins/bar/widgets/KeyboardLayoutModel.js')
const japanese = { methods: ['keyboard-us', 'mozc'], current: 'mozc', name: 'Mozc', language: 'ja' }
assertEqual(model.inputLabel(japanese, 'EN'), 'あ', 'Japanese input has a distinct active label')
assertEqual(model.inputLabel({ ...japanese, label: 'ア' }, 'EN'), 'ア', 'the live engine mode takes precedence over its default glyph')
assertEqual(model.inputLabel({ ...japanese, current: 'keyboard-us' }, 'EN'), 'EN', 'Latin input retains the keyboard label')
assertEqual(model.inputLabel({}, 'EN'), 'EN', 'a stopped input service retains the keyboard label')
assertEqual(model.inputLabel({ current: 'hangul' }, 'EN'), '한', 'Korean input has a distinct label')
assertEqual(model.inputLabel({ current: 'custom', language: 'vi' }, 'EN'), 'VI', 'other engines use their reported language')
assertEqual(model.inputTooltip(japanese, 'English (US)', false).includes('Space twice'), true, 'Japanese explains character choices')
assertEqual(model.inputTooltip(japanese, 'English (US)', false).includes('Ctrl + Shift + Space'), true, 'the input toggle shortcut is discoverable')
assertEqual(model.inputTooltip(japanese, 'English (US)', true).includes('Right-click'), true, 'combined input and layout switching stays discoverable')
assertEqual(model.inputTooltip({ methods: ['keyboard-fr'] }, 'French', false), 'French\nClick to set up an input language', 'one foreign mode offers input setup')
assertEqual(model.showIndicator('EN', false, false), false, 'English alone stays hidden')
assertEqual(model.showIndicator('ENG', false, false), false, 'English before the brief table loads stays hidden')
assertEqual(model.showIndicator('FR', false, false), true, 'a single non-English layout stays available')
assertEqual(model.showIndicator('EN', false, true), true, 'Latin with another input mode stays available')
assertEqual(model.showIndicator('EN', true, false), true, 'multiple keyboard layouts stay available')
// Trimmed from xkbcli list, keeping the format of every section it prints,
// including the options nested under an option group: those quote their
// description and print an empty brief, one indent deeper than a layout's.
+212 -1
View File
@@ -32,7 +32,7 @@ for status in 1 2 3; do
else
(( result != 0 )) && ! grep -q '^gum ' "$test_tmp/commands" || fail "inspection errors must not offer enablement"
fi
grep -q '^sudo -N -- .* __status ' "$test_tmp/commands" || fail "status must not publish reusable authorization"
grep -q '^sudo -n -N -l -l -- .* __status ' "$test_tmp/commands" || fail "status must inspect policy without authentication"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]] || fail "public exit must revoke its authorization"
done
pass "public status distinguishes inactive from errors and revokes authorization on exit"
@@ -164,3 +164,214 @@ cp "$ROOT/default/libalpm/hooks/05-omarchy-passwordless-revoke.hook" "$test_tmp/
[[ ! -e $(rule_file 1000) ]]
)
pass "trailing-dollar accounts publish valid native policy and alias-shaped names never publish"
reset_grant
(
source "$library"
valid_duration permanent
! valid_duration forever || fail "invalid duration was accepted"
enable_locked 1000 permanent
read_grant 1000
[[ $GRANT_NAME == audituser && -z $GRANT_DEADLINE ]]
[[ $(stat -c '%a' "$(rule_file 1000 permanent)") == 440 ]]
/usr/sbin/visudo -cf "$(rule_file 1000 permanent)" >/dev/null
! grep -q '^systemd-run ' "$test_tmp/commands" || fail "permanent grant started a timer"
TEST_EXPIRED=1 status_locked 1000
TEST_EXPIRED=1 expire_locked 1000
cleanup_all_locked
/usr/bin/systemd-tmpfiles --root="$test_tmp" --remove --boot --inline 'r! /etc/sudoers.d/99-omarchy-nopasswd-*'
[[ -f $(rule_file 1000 permanent) ]]
cleanup_uid_locked 1000
assert_status 3 status_locked 1000
)
pass "permanent access has no timer, survives expiry and temporary cleanup, and can be disabled"
reset_grant
(
source "$library"
enable_locked 1000 15
enable_locked 1000 permanent
[[ ! -e $(rule_file 1000) && -e $(rule_file 1000 permanent) ]]
TEST_EXPIRED=1 expire_locked 1000
[[ -e $(rule_file 1000 permanent) ]]
enable_locked 1000 60
[[ -e $(rule_file 1000) && ! -e $(rule_file 1000 permanent) ]]
TEST_EXPIRED=1 expire_locked 1000
assert_status 3 status_locked 1000
TEST_PUBLISH_FAIL=1 assert_status 1 enable_locked 1000 permanent
[[ ! -e $(rule_file 1000 permanent) ]]
)
pass "switching duration replaces the prior policy and old callbacks preserve permanent access"
reset_grant
(
source "$library"
permanent=$(rule_file 1000 permanent)
printf 'audituser ALL=(ALL) NOPASSWD: /usr/bin/true\n' >"$permanent"
assert_status 2 status_locked 1000
assert_status 1 enable_locked 1000 permanent
assert_status 1 cleanup_uid_locked 1000
[[ -e $permanent ]]
rm "$permanent"
enable_locked 1000 permanent
TEST_BAD_PATH="$permanent" assert_status 2 status_locked 1000
)
pass "permanent policy rejects unsafe ownership and preserves administrator edits"
for choice in '15 minutes' '1 Hour' '1 Day' Permanently; do
case "$choice" in
'15 minutes') expected=15 ;;
'1 Hour') expected=60 ;;
'1 Day') expected=1440 ;;
Permanently) expected=permanent ;;
esac
: >"$test_tmp/commands"
TEST_CHOICE="$choice" TEST_CONFIRM_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
grep -q '^gum choose .*15 minutes 1 Hour 1 Day Permanently$' "$test_tmp/commands"
grep -q "^sudo -N -- .* __enable .* $expected$" "$test_tmp/commands"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]]
if [[ $expected == permanent ]]; then
grep -q 'remain enabled across reboots until you disable it' "$test_tmp/public.log"
! grep -q 'automatically disable' "$test_tmp/public.log" || fail "permanent grant claims automatic expiry"
fi
done
pass "each duration choice dispatches its exact grant duration with permanent-specific copy"
: >"$test_tmp/commands"
assert_status 130 env TEST_CHOICE_CANCEL=1 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
! grep -q '__enable\|^gum confirm ' "$test_tmp/commands" || fail "cancelled picker continued enablement"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]]
: >"$test_tmp/commands"
TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
! grep -q '^gum ' "$test_tmp/commands" || fail "disabling access offered a picker"
grep -q '__disable ' "$test_tmp/commands"
pass "cancelling the picker grants nothing and active access still toggles off"
for confirm_status in 0 1; do
: >"$test_tmp/commands"
TEST_STATUS=0 TEST_CONFIRM_STATUS=$confirm_status /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" permanent >"$test_tmp/public.log" 2>&1
grep -q '^gum confirm Enable passwordless sudo permanently?' "$test_tmp/commands"
if (( confirm_status == 0 )); then
grep -q '__enable .* permanent$' "$test_tmp/commands"
else
! grep -q '__enable ' "$test_tmp/commands" || fail "declining permanent access still enabled it"
fi
done
pass "changing active access to permanent always requires confirmation"
reset_grant
(
source "$library"
enable_locked 1000 15
TEST_REQUIRE_EXISTING_TARGET=1 enable_locked 1000 60
cleanup_uid_locked 1000
enable_locked 01000 permanent
status_locked 1000
[[ -e $(rule_file 1000 permanent) ]]
[[ ! -e $test_tmp/etc/sudoers.d/99-omarchy-permanent-nopasswd-01000 ]]
cleanup_uid_locked 1000
assert_status 3 status_locked 01000
)
pass "renewal replaces the existing rule atomically and zero-padded UIDs share one policy"
for policy in ' Options: !authenticate' ' Options: authenticate'; do
: >"$test_tmp/commands"
expected=1
[[ $policy != *'!authenticate'* ]] || expected=0
assert_status "$expected" env TEST_POLICY="$policy" /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
[[ $(wc -l <"$test_tmp/commands") == 1 ]]
grep -q '^sudo -n -N -l -l -- .* __status ' "$test_tmp/commands"
done
assert_status 1 env TEST_POLICY_FAILURE=1 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
pass "menu probe reads policy tags without executing a privileged action or changing the timestamp"
for status in 0 3; do
: >"$test_tmp/commands"
TEST_STATUS=$status /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --disable >"$test_tmp/public.log" 2>&1
! grep -q '^gum\|__enable ' "$test_tmp/commands" || fail "explicit disable offered enablement"
if (( status == 0 )); then
grep -q '__disable ' "$test_tmp/commands"
fi
done
pass "explicit disable never enables access even when a displayed grant has expired"
: >"$test_tmp/commands"
TEST_STATUS=3 TEST_CHOICE='15 minutes' TEST_CONFIRM_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" >"$test_tmp/public.log" 2>&1
[[ $(grep -c '^sudo -N -- ' "$test_tmp/commands") == 1 ]] || fail "enable must authenticate exactly one sudo call"
grep -q '^sudo -N -- .* __enable .* 15$' "$test_tmp/commands"
! grep -q '^sudo -n -N -- .* __status ' "$test_tmp/commands" || fail "inactive flow attempted root status"
[[ $(tail -1 "$test_tmp/commands") == 'sudo -k' ]]
pass "enabling from inactive policy has exactly one password-capable sudo invocation"
: >"$test_tmp/commands"
TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --disable >"$test_tmp/public.log" 2>&1
grep -q '^sudo -n -N -- .* __disable ' "$test_tmp/commands" || fail "disable must refuse interactive authentication"
! grep -q '^sudo -N -- ' "$test_tmp/commands" || fail "disable attempted an interactive sudo command"
pass "disabling active access is fully noninteractive"
for duration in permanent 15; do
reset_grant
(
source "$library"
if [[ $duration == permanent ]]; then
enable_locked 1000 15
else
enable_locked 1000 permanent
fi
assert_status 137 env TEST_KILL_AFTER_PUBLISH=1 TEST_EUID=0 SUDO_UID=1000 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" __enable 1000 "$duration"
[[ -f $(rule_file 1000) && -f $(rule_file 1000 permanent) ]]
status_locked 1000
TEST_EXPIRED=1 expire_locked 1000
[[ -f $(rule_file 1000 permanent) ]]
cleanup_uid_locked 1000
assert_status 3 status_locked 1000
)
done
pass "SIGKILL during either duration switch leaves access inspectable and revocable without expiring permanent policy"
reset_grant
(
source "$library"
enable_locked 1000 permanent
timed=$(rule_file 1000)
printf 'otheruser ALL=(ALL) NOTAFTER=99991231235959Z NOPASSWD: ALL\n' >"$timed"
assert_status 2 read_grant 1000
assert_status 1 enable_locked 1000 15
printf 'audituser ALL=(ALL) NOTAFTER=99991231235959Z NOPASSWD: ALL\n' >"$timed"
TEST_BAD_PATH="$timed" assert_status 2 read_grant 1000
rm "$timed"
ln -s "$(rule_file 1000 permanent)" "$timed"
assert_status 2 read_grant 1000
rm "$timed"
printf 'audituser ALL=(ALL) NOTAFTER=99991231235959Z NOPASSWD: ALL\n' >"$timed"
enable_locked 1000 15
[[ -f $timed && ! -e $(rule_file 1000 permanent) ]]
)
pass "paired policy recovery rejects mismatched or unsafe rules and allows a complete duration change"
: >"$test_tmp/commands"
TEST_POLICY_FAILURE=1 TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --disable >"$test_tmp/public.log" 2>&1
grep -q '^sudo -n -N -- .* __disable ' "$test_tmp/commands"
! grep -q '^gum\|^sudo -N -- ' "$test_tmp/commands" || fail "listpw=always disable prompted"
TEST_POLICY_FAILURE=1 TEST_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
assert_status 1 env TEST_POLICY_FAILURE=1 TEST_STATUS=1 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
: >"$test_tmp/commands"
TEST_POLICY_FAILURE=1 TEST_STATUS=1 TEST_CONFIRM_STATUS=0 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" 15 >"$test_tmp/public.log" 2>&1
[[ $(grep -c '^sudo -N -- ' "$test_tmp/commands") == 1 ]] || fail "listing failure must leave one enable authentication"
grep -q '^sudo -N -- .* __enable .* 15$' "$test_tmp/commands"
: >"$test_tmp/commands"
assert_status 1 env TEST_POLICY_FAILURE=1 TEST_STATUS=2 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" 15 >"$test_tmp/public.log" 2>&1
! grep -q '^gum\|__enable ' "$test_tmp/commands" || fail "unsafe grant was offered enablement"
pass "password-required listings do not block enabling, disabling, or active detection"
: >"$test_tmp/commands"
TEST_POLICY_FAILURE=1 TEST_STATUS=2 /usr/bin/bash -p "$test_tmp/omarchy-sudo-passwordless" --active
grep -q '^sudo -kn -- .* __status ' "$test_tmp/commands" || fail "fallback probe must ignore cached credentials"
(
source "$library"
with_root_lock() { return 1; }
verify_sudo_caller() { return 0; }
assert_status 2 root_dispatch __status 1000
)
pass "status lock failures remain inspection errors and an authenticated unsafe grant keeps its warning"
+4
View File
@@ -271,6 +271,10 @@ const notification = {
expireTimeout: 1.5
}
const snapshot = notifications.snapshotOf(notification, 12345)
assertDeepEqual(JSON.parse(notifications.snapshotOf({ actions: [
{ identifier: 'default', text: 'Open' }, { identifier: 'yes', text: 'Yes' }, { identifier: 'no', text: 'No' }
]}).actionsJson), [{ identifier: 'yes', text: 'Yes' }, { identifier: 'no', text: 'No' }], 'explicit choices appear independently of the default card action')
assertEqual(notifications.popupEntry({ actionsJson: '[{"identifier":"yes","text":"Yes"}]' }, 1).actionsJson, '[]', 'restored notifications do not replay actions from a dead sender')
assertDeepEqual(
{
id: snapshot.id,
+8 -1
View File
@@ -6,11 +6,12 @@ work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/bin" "$work/home"
export TEST_CALLS="$work/calls"
for command in omarchy-pkg-add omarchy-hook-install systemctl; do
for command in omarchy-pkg-add omarchy-hook-install omarchy-input-method systemctl; do
cat >"$work/bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$TEST_CALLS"
case "$*" in
'configure --defaults') [[ ${TEST_INPUT_FAIL:-0} == 0 ]] ;;
'--user enable owed.service') [[ ${TEST_TTY:-0} == 0 ]] ;;
'--user is-active --quiet graphical-session.target') [[ ${TEST_TTY:-0} == 0 ]] ;;
'--user start owed.service') [[ ${TEST_START_FAIL:-0} == 0 ]] ;;
@@ -58,6 +59,12 @@ grep -E '^systemctl --user enable --now .*owed.service' "$TEST_CALLS" >/dev/null
grep -Fx 'omarchy-hook-install theme-set /usr/share/owe/10-owe-sync' "$TEST_CALLS" >/dev/null
pass "fresh installs enable OWE and install the theme refresh hook"
: >"$TEST_CALLS"
HOME="$work/home" PATH="$work/bin:$PATH" TEST_INPUT_FAIL=1 bash "$ROOT/install/user/first-run/enable-user-units.sh"
grep -Fx 'omarchy-input-method configure --defaults' "$TEST_CALLS" >/dev/null
grep -E '^systemctl --user enable --now .*owed.service' "$TEST_CALLS" >/dev/null
pass "failed input defaults do not block enabling first-run services"
cat >"$work/bin/ffmpegthumbnailer" <<'SH'
#!/bin/bash
printf 'thumbnail\n' >>"$TEST_CALLS"
@@ -0,0 +1,36 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const read = name => fs.readFileSync(path.join(root, name), 'utf8')
const indicator = read('shell/plugins/bar/indicators/PasswordlessSudo.qml')
const widget = read('shell/plugins/bar/widgets/Indicators.qml')
const manifest = JSON.parse(read('shell/plugins/bar/widgets/Indicators.manifest.json'))
assert(widget.match(/defaultIndicatorEntries: \[ "PasswordlessSudo", "Dictation"/), 'passwordless sudo is included in the default indicator tray')
assert(manifest.barWidget.schema.find(field => field.key === 'items').options.some(option => option.value === 'PasswordlessSudo'), 'passwordless sudo is configurable alongside Night Light')
assert(indicator.includes('useActiveColor: true') && indicator.includes('activeColor: Color.urgent'), 'active passwordless sudo uses the theme danger color')
assertEqual(indicator.match(/activeText: "([^"]+)"/)[1], indicator.match(/inactiveText: "([^"]+)"/)[1], 'sudo keeps the same icon in both states')
assert(!widget.includes('sudoHorizontal') && !widget.includes('sudoVertical'), 'sudo participates in the normal indicator blocks')
assert(!indicator.includes('visible:'), 'sudo uses the shared indicator visibility and hover behavior')
assert(indicator.includes('command: ["omarchy-sudo-passwordless", "--active"]'), 'indicator uses the noninteractive grant probe')
assert(indicator.includes('root.granted = exitCode === 0 && exitStatus === 0'), 'failed or interrupted probes do not claim an active grant')
assert(indicator.includes('interval: 5000') && indicator.includes('onTriggered: root.refresh()'), 'indicator refreshes after activation, revocation, and expiry')
assert(indicator.includes('command: ["omarchy-sudo-passwordless", "--disable"]'), 'active indicator revokes access through a background process')
assert(indicator.includes('if (root.granted) disableProc.running = true'), 'active click bypasses the terminal launcher')
assert(indicator.includes('root.indicatorHost.refresh()'), 'disabling access refreshes all indicator instances immediately')
const press = new Function('root', 'disableProc', indicator.match(/onPressed: function\(\) \{([\s\S]*?)\n \}/)[1])
const launched = []
const button = { granted: true, bar: { run: command => launched.push(command) } }
const revoke = { running: false }
press(button, revoke)
assert(revoke.running && launched.length === 0, 'clicking active sudo starts background revocation without a terminal')
button.granted = false
press(button, revoke)
assert(launched.length === 0, 'repeat clicks during revocation cannot open the enable flow')
revoke.running = false
press(button, revoke)
assertDeepEqual(launched, ['omarchy-launch-floating-terminal-with-presentation omarchy-sudo-passwordless'], 'inactive click still opens interactive setup')
JS
+9 -12
View File
@@ -14,10 +14,10 @@ rewrite() { sed -e "s|/etc/|$root/etc/|g" -e "s| /\\.cargo/| $root/.cargo/|g" "$
rewrite "$ROOT/install/config/region.sh" "$sandbox/install/config/region.sh"
rewrite "$ROOT/bin/omarchy-region" "$sandbox/bin/omarchy-region"
for command in omarchy-update-pacman omarchy-refresh-pacman locale-gen rime-ice-installer omarchy-setup-claude-api systemctl omarchy-system-logout pacman-key pacman omarchy-apply-pacman; do
for command in omarchy-refresh-pacman locale-gen omarchy-setup-claude-api omarchy-system-logout pacman-key pacman omarchy-apply-pacman; do
printf '#!/bin/bash\necho "%s $*" >>"%s"\n' "$command" "$calls" >"$test_tmp/bin/$command"
done
printf '#!/bin/bash\necho "omarchy-pkg-add $*" >>"%s"\n[[ -z ${STUB_PKG_FAIL:-} ]]\n' "$calls" >"$test_tmp/bin/omarchy-pkg-add"
printf '#!/bin/bash\necho "omarchy-update-pacman $*" >>"%s"\n[[ -z ${STUB_UPDATE_FAIL:-} ]]\n' "$calls" >"$test_tmp/bin/omarchy-update-pacman"
printf '#!/bin/bash\necho "${STUB_CHANNEL:-stable}"\n' >"$test_tmp/bin/omarchy-version-channel"
printf '#!/bin/bash\n[[ $1 == env ]] && shift\nexec env "$@"\n' >"$test_tmp/bin/sudo"
printf '#!/bin/bash\necho "gum $*" >>"%s"\nexit "${GUM_ANSWER:-1}"\n' "$calls" >"$test_tmp/bin/gum"
@@ -32,17 +32,15 @@ printf 'LANG=en_US.UTF-8\n' >"$root/etc/locale.conf"
omarchy-region cn >/dev/null
[[ $(omarchy-region) == "China" && $(<"$root/etc/omarchy/region") == "cn" && $(<"$root/etc/locale.conf") == "LANG=zh_CN.UTF-8" ]] ||
fail "switching to China records cn and sets the language"
[[ $(switch_calls) == $'omarchy-update-pacman -Syu --noconfirm\nomarchy-pkg-add fcitx5-configtool fcitx5-rime librime opencc rime-ice-installer\nlocale-gen \nomarchy-apply-pacman --render stable RENDERED\npacman --config RENDERED/pacman.conf -Sy --noconfirm --needed archlinuxcn-keyring\npacman-key --populate archlinuxcn\nomarchy-refresh-pacman stable\nsystemctl --user try-restart omarchy-fcitx5.service\nrime-ice-installer --yes' ]] ||
fail "China installs Rime, trusts ArchLinuxCN through the rendered cn config, refreshes, then sets Rime Ice up" "$(switch_calls)"
[[ $(switch_calls) == $'omarchy-update-pacman -Syu --noconfirm\nlocale-gen \nomarchy-apply-pacman --render stable RENDERED\npacman --config RENDERED/pacman.conf -Sy --noconfirm --needed archlinuxcn-keyring\npacman-key --populate archlinuxcn\nomarchy-refresh-pacman stable' ]] ||
fail "China upgrades, trusts ArchLinuxCN through the rendered cn config, then refreshes" "$(switch_calls)"
[[ -z $(ls -A "$test_tmp/tmp") ]] || fail "the rendered config is removed"
pass "switching to China installs Rime, trusts ArchLinuxCN before the refresh upgrades through it"
pass "switching to China trusts ArchLinuxCN before the refresh upgrades through it"
: >"$calls"
mkdir -p "$HOME/.local/share/fcitx5/rime" && touch "$HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml"
STUB_CHANNEL=edge omarchy-region cn >/dev/null
grep -qx 'omarchy-refresh-pacman edge' "$calls" || fail "the refresh keeps the machine's channel" "$(<"$calls")"
grep -q '^rime-ice-installer\|^systemctl' "$calls" && fail "an existing Rime Ice setup is not redone"
pass "switching to China again keeps the channel and redoes no Rime Ice setup"
pass "switching to China again keeps the channel"
: >"$calls"
omarchy-region global >/dev/null
@@ -50,8 +48,7 @@ omarchy-region global >/dev/null
fail "switching to Global removes the region and restores English"
[[ $(<"$calls") == $'omarchy-refresh-pacman stable\nomarchy-setup-claude-api --remove' ]] ||
fail "Global refreshes pacman back to the channel defaults and removes the Claude API setup" "$(<"$calls")"
[[ -f $HOME/.local/share/fcitx5/rime/rime_ice.schema.yaml ]] || fail "Global keeps the input method"
pass "switching to Global restores the channel's pacman defaults and English, keeps Rime, removes the Claude API setup"
pass "switching to Global restores the channel's pacman defaults and English, removes the Claude API setup"
if output=$(omarchy-region mars 2>&1); then
fail "an unknown region is refused" "$output"
@@ -63,8 +60,8 @@ fi
pass "an unknown region or an unclear channel is refused"
: >"$calls"
if output=$(STUB_PKG_FAIL=1 omarchy-region cn 2>&1); then
fail "switching to China fails when its packages do not install" "$output"
if output=$(STUB_UPDATE_FAIL=1 omarchy-region cn 2>&1); then
fail "switching to China fails when the upgrade fails" "$output"
fi
[[ $(omarchy-region) == "Global" ]] && ! grep -q '^omarchy-refresh-pacman' "$calls" ||
fail "a failed switch to China leaves the region Global and pacman untouched" "$(<"$calls")"
+3 -37
View File
@@ -29,11 +29,7 @@ pass "an unknown region fails and names the file and value"
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls"
mkdir -p "$stub_bin"
for command in omarchy-pkg-add rime-ice-installer locale-gen; do
printf '#!/bin/bash\necho "%s $*" >>"%s"\n' "$command" "$calls" >"$stub_bin/$command"
done
printf '#!/bin/bash\necho "omarchy-pkg-add $*" >>"%s"\n[[ -z ${STUB_PKG_FAIL:-} ]]\n' "$calls" >"$stub_bin/omarchy-pkg-add"
printf '#!/bin/bash\necho "$STUB_REGION"\n' >"$stub_bin/omarchy-region"
printf '#!/bin/bash\necho "locale-gen $*" >>"%s"\n' "$calls" >"$stub_bin/locale-gen"
chmod +x "$stub_bin"/*
PATH="$stub_bin:$PATH"
@@ -48,14 +44,10 @@ printf 'LANG=en_US.UTF-8\nLC_TIME=en_DK.UTF-8\n' >"$root/etc/locale.conf"
: >"$calls"
OMARCHY_REGION=cn bash -e "$region_setup"
[[ $(<"$root/etc/omarchy/region") == "cn" ]] || fail "install records the China region as cn"
[[ $(<"$calls") == $'omarchy-pkg-add fcitx5-configtool fcitx5-rime librime opencc rime-ice-installer\nlocale-gen ' ]] ||
fail "China installs the Rime input method packages and generates the Chinese locale" "$(<"$calls")"
[[ $(<"$calls") == "locale-gen " ]] || fail "China generates the Chinese locale" "$(<"$calls")"
[[ $(<"$root/etc/locale.gen") == $'zh_CN.UTF-8 UTF-8\nen_US.UTF-8 UTF-8' ]] || fail "China enables zh_CN in locale.gen"
[[ $(<"$root/etc/locale.conf") == $'LANG=zh_CN.UTF-8\nLC_TIME=en_DK.UTF-8' ]] || fail "China sets LANG and keeps other locale settings" "$(<"$root/etc/locale.conf")"
for package in fcitx5-configtool fcitx5-rime librime opencc rime-ice-installer; do
grep -qx "$package" "$ROOT/install/omarchy-other.packages" || fail "the ISO mirror carries $package"
done
pass "install records the China region and installs the Rime packages the ISO carries"
pass "install records the China region and sets the Chinese locale"
for link in "environment.conf etc/environment.d/90-omarchy-china.conf" "cargo.toml .cargo/config.toml" "gemrc etc/gemrc"; do
read -r source target <<<"$link"
@@ -66,7 +58,6 @@ pass "China links the developer mirror drop-ins"
: >"$calls"
OMARCHY_REGION=global bash -e "$region_setup"
[[ ! -e $root/etc/omarchy/region ]] || fail "install records Global by leaving no region file"
[[ ! -s $calls ]] || fail "Global installs no input method packages" "$(<"$calls")"
[[ $(<"$root/etc/locale.conf") == $'LANG=en_US.UTF-8\nLC_TIME=en_DK.UTF-8' ]] || fail "Global turns a Chinese LANG back to English" "$(<"$root/etc/locale.conf")"
printf 'LANG=de_DE.UTF-8\n' >"$root/etc/locale.conf"
OMARCHY_REGION=global bash -e "$region_setup"
@@ -91,12 +82,6 @@ fi
rm "$root/etc/gemrc"
pass "region setup never touches mirror config Omarchy did not create"
if output=$(STUB_PKG_FAIL=1 OMARCHY_REGION=cn bash -e "$region_setup" 2>&1); then
fail "China fails when its packages do not install" "$output"
fi
[[ ! -e $root/etc/omarchy/region && ! -e $root/etc/gemrc && $(<"$root/etc/locale.conf") == "LANG=en_US.UTF-8" ]] ||
fail "a failed package install leaves the machine Global"
pass "China installs its packages before it writes any region state"
echo cn >"$root/etc/omarchy/region"
: >"$calls"
env -u OMARCHY_REGION bash -e "$region_setup"
@@ -113,22 +98,3 @@ if output=$(OMARCHY_REGION=mars bash -e "$region_setup" 2>&1); then
fi
[[ $output == "Unknown region: mars" ]] || fail "install names the unknown region" "$output"
pass "install rejects an unknown region"
user_region="$ROOT/install/user/region.sh"
: >"$calls"
STUB_REGION=China OMARCHY_SETUP_CONTEXT=iso-chroot bash -e "$user_region"
[[ $(<"$calls") == "rime-ice-installer --yes --asset-dir /opt/packages/rime-ice" ]] ||
fail "a China install sets up Rime Ice from the ISO's assets" "$(<"$calls")"
pass "a China install sets up Rime Ice from the ISO's assets"
: >"$calls"
output=$(STUB_REGION=China OMARCHY_SETUP_CONTEXT=provision-owner bash -e "$user_region")
[[ ! -s $calls && $output == "Skipping Rime Ice setup in provision-owner: only the ISO bundles its assets" ]] ||
fail "first-boot setup says why it skips Rime Ice" "$output $(<"$calls")"
pass "first-boot setup says why it skips Rime Ice"
: >"$calls"
output=$(STUB_REGION=Global OMARCHY_SETUP_CONTEXT=iso-chroot bash -e "$user_region")
[[ ! -s $calls && -z $output ]] || fail "a Global install leaves the input method alone" "$output $(<"$calls")"
pass "a Global install leaves the input method alone"
+15
View File
@@ -66,6 +66,8 @@ trap 'if [[ ${FUNCNAME[0]:-} == "$PROMPT_FN" ]]; then printf "returned\n" >>"$MA
printf 'region=%s\n' "${region:-}"
printf 'keyboard=%s\n' "${keyboard:-}"
printf 'keyboard_label=%s\n' "${keyboard_label:-}"
printf 'keyboard_input_method=%s\n' "${keyboard_input_method:-}"
printf 'keyboard_xkb_layout=%s\n' "${keyboard_xkb_layout:-}"
printf 'username=%s\n' "${username:-}"
printf 'password=%s\n' "${password:-}"
printf 'password_confirmation=%s\n' "${password_confirmation:-}"
@@ -167,6 +169,19 @@ assert_status 0 "keyboard prompt succeeds"
grep -qF -- '--selected English (US)' "$GUM_ARGS" || fail "keyboard prompt preselects English (US)"
pass "keyboard prompt maps the chosen label to its keymap"
for selection in 'Japanese|jp106|mozc|' 'Japanese (US keyboard)|us|mozc|' 'Korean|us|hangul|kr' 'Chinese (Simplified, Pinyin)|us|pinyin|' 'Chinese (Traditional, Zhuyin)|us|chewing|'; do
IFS='|' read -r label keymap method layout <<< "$selection"
run_prompt omarchy_prompt_keyboard "0:$label"
assert_status 0 "$label can be selected at install"
[[ $(field keyboard) == "$keymap" && $(field keyboard_input_method) == "$method" && $(field keyboard_xkb_layout) == "$layout" ]] ||
fail "$label carries a real console keymap and a separate input preference"
done
pass "language keyboard choices carry their keymap, input method, and desktop override"
keyboard_input_method=hangul keyboard_xkb_layout=kr omarchy_persist_input_selection "$tmp_dir/target"
[[ $(<"$tmp_dir/target/etc/omarchy/input-method") == $'INPUT_METHOD=hangul\nXKB_LAYOUT=kr' ]] || fail "first-boot setup persists the selection for user finalization"
pass "first-boot setup persists the selection for user finalization"
run_prompt omarchy_prompt_keyboard "1:"
assert_status "$OMARCHY_FORM_BACK" "keyboard prompt reports Esc as back"
assert_returned "keyboard prompt survives Esc under set -e"
+1 -1
View File
@@ -71,7 +71,7 @@ grep -Fx 'WantedBy=graphical-session.target' "$fcitx_service" >/dev/null ||
grep -Fx 'ConditionEnvironment=WAYLAND_DISPLAY' "$fcitx_service" >/dev/null ||
fail "an update over SSH has a live user manager and no display; starting fcitx5 there wedges the unit active-but-blind, and Wants= will not replace it at graphical login"
grep -F 'pkill -x fcitx5' "$ROOT/bin/omarchy-restart-xcompose" >/dev/null ||
grep -F 'pkill -u "$UID" -x fcitx5' "$ROOT/bin/omarchy-restart-xcompose" >/dev/null ||
fail "restart-xcompose cannot reload a fcitx5 running outside the unit, so it silently keeps serving the old table"
grep -F 'omarchy-fcitx5.service' "$first_run_units" >/dev/null ||
+25 -3
View File
@@ -23,7 +23,7 @@ SH
}
run_orphan_checker() {
HOME="$test_home" PATH="$stub_bin:$PATH" "$ROOT/bin/omarchy-update-orphan-pkgs"
HOME="$test_home" PATH="$stub_bin:$ROOT/bin:$PATH" "$ROOT/bin/omarchy-update-orphan-pkgs" "$@"
}
write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then printf "old-lib\nunused-tool\n"; exit 0; fi; exit 1'
@@ -35,7 +35,29 @@ grep -q '^ old-lib$' "$test_tmp/noninteractive.out" || fail "orphan checker lis
grep -q 'Re-run omarchy-update-orphan-pkgs in a terminal' "$test_tmp/noninteractive.out" || fail "orphan checker does not remove packages non-interactively"
pass "orphan checker only reports orphans non-interactively"
export REMOVAL_LOG="$test_tmp/removal.log"
write_stub pacman 'case $1 in
-Qtdq|-Qq) printf "old-lib\nunused-tool\n" ;;
-Rns) printf "%s\n" "$@" >"$REMOVAL_LOG" ;;
*) exit 1 ;;
esac'
write_stub sudo '"$@"'
run_orphan_checker -y >"$test_tmp/approved.out" 2>"$test_tmp/approved.err"
diff <(printf '%s\n' -Rns --noconfirm old-lib unused-tool) "$REMOVAL_LOG" ||
fail "approved cleanup removes the orphan packages without pacman confirmation"
pass "approved cleanup removes orphans without Omarchy or pacman confirmation"
write_stub sudo 'exit 42'
if run_orphan_checker -y >"$test_tmp/failure.out" 2>"$test_tmp/failure.err"; then
fail "approved cleanup reports removal failures"
fi
pass "approved cleanup reports removal failures"
rm -f "$REMOVAL_LOG"
write_stub pacman 'if [[ $1 == "-Qtdq" ]]; then exit 0; fi; exit 1'
run_orphan_checker >"$test_tmp/none.out" 2>"$test_tmp/none.err"
[[ ! -s $test_tmp/none.out ]] || fail "orphan checker stays quiet when no orphans exist"
for mode in "" -y; do
run_orphan_checker $mode >"$test_tmp/none.out" 2>"$test_tmp/none.err"
[[ ! -s $test_tmp/none.out ]] || fail "orphan checker stays quiet when no orphans exist"
done
[[ ! -e $REMOVAL_LOG ]] || fail "orphan checker does not remove packages when no orphans exist"
pass "orphan checker stays quiet without orphans"
+7
View File
@@ -35,6 +35,9 @@ for step in "${steps[@]}"; do
cat >"$stub_bin/$step" <<'STUB'
#!/bin/bash
printf '%s unattended=%s\n' "${0##*/}" "${OMARCHY_UPDATE_UNATTENDED:-}" >>"$STEP_LOG"
if [[ ${0##*/} == "omarchy-update-orphan-pkgs" ]]; then
printf '%s\n' "$@" >"$STEP_LOG.orphan-args"
fi
[[ ${FAILING_STEP:-} != "${0##*/}" ]] || exit 1
STUB
chmod +x "$stub_bin/$step"
@@ -84,6 +87,8 @@ run_update -y || fail "an update where everything works reports a failure"
diff <(expected_steps) <(steps_run) >"$test_tmp/order" ||
fail "an update where everything works does not run every step in order" "$(cat "$test_tmp/order")"
pass "an update where every step works runs all of them, in order"
[[ $(cat "$test_tmp/steps.orphan-args") == "-y" ]] || fail "an unattended update approves orphan removal"
pass "an unattended update approves orphan removal"
grep -q '^omarchy-update-system-pkgs unattended=1$' "$test_tmp/steps" ||
fail "-y does not mark the update unattended"
@@ -93,6 +98,8 @@ diff <(expected_steps confirmed) <(steps_run) >"$test_tmp/order" ||
grep -q '^omarchy-update-system-pkgs unattended=$' "$test_tmp/steps" ||
fail "an update a person confirmed is treated as unattended"
pass "-y is what marks an update unattended, not the update itself"
[[ $(cat "$test_tmp/steps.orphan-args") == "-y" ]] || fail "a confirmed update approves orphan removal"
pass "a confirmed update approves orphan removal"
# Migrations ship with the packages the upgrade installs and are written against
# them. Running them against what is still on disk is the failure this ordering
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
leaf="$ROOT/install/hardware/dell-xps13-ptl-speaker-firmware.sh"
migration="$ROOT/migrations/1791265613.sh"
grep -q 'run_logged .*hardware/dell-xps13-ptl-speaker-firmware.sh' "$ROOT/install/hardware/all.sh" ||
fail "hardware setup installs the Panther Lake XPS 13 firmware"
pass "hardware setup installs the Panther Lake XPS 13 firmware"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
mkdir -p "$test_tmp/bin"
cat >"$test_tmp/bin/omarchy-hw-match" <<'SH'
#!/bin/bash
[[ $TEST_PRODUCT_NAME == *"$1"* ]]
SH
cat >"$test_tmp/bin/omarchy-hw-intel-ptl" <<'SH'
#!/bin/bash
[[ $TEST_INTEL_PTL == "1" ]]
SH
cat >"$test_tmp/bin/sudo" <<'SH'
#!/bin/bash
if [[ $1 == "pacman" ]]; then
exec "$@"
fi
printf 'Unexpected privileged command: %s\n' "$*" >&2
exit 1
SH
cat >"$test_tmp/bin/pacman" <<'SH'
#!/bin/bash
if [[ $1 == "-Q" ]]; then
if [[ ${@: -1} == "linux-firmware-cirrus-dx13260" ]]; then
[[ -e $TEST_ALIAS_FILE ]] || exit 1
printf 'linux-firmware-cirrus-dx13260 20260810-1\n'
else
[[ -s $TEST_VERSION_FILE ]] || exit 1
printf 'linux-firmware-cirrus %s\n' "$(<"$TEST_VERSION_FILE")"
fi
else
printf 'pacman %s\n' "$*" >>"$TEST_LOG"
[[ $* == "-S --noconfirm --needed -- linux-firmware-cirrus-dx13260" ]] || exit 1
[[ ${TEST_INSTALL_FAILURE:-0} == "0" ]] || exit 1
touch "$TEST_ALIAS_FILE"
fi
SH
cat >"$test_tmp/bin/omarchy-state" <<'SH'
#!/bin/bash
printf 'state %s\n' "$*" >>"$TEST_LOG"
SH
chmod +x "$test_tmp/bin/"*
export PATH="$test_tmp/bin:$ROOT/bin:$PATH"
export TEST_LOG="$test_tmp/calls" TEST_VERSION_FILE="$test_tmp/version" TEST_ALIAS_FILE="$test_tmp/alias-installed"
export TEST_PRODUCT_NAME="XPS 13 DX13260" TEST_INTEL_PTL=1
install_call="pacman -S --noconfirm --needed -- linux-firmware-cirrus-dx13260"
export OMARCHY_PATH="$ROOT"
reset_fixture() {
: >"$TEST_LOG"
printf '%s' "$1" >"$TEST_VERSION_FILE"
rm -f "$TEST_ALIAS_FILE"
}
run_leaf() {
bash -eE -c 'source "$1"' bash "$leaf"
}
run_migration() {
bash -euo pipefail "$migration" >/dev/null
}
for model in "XPS 9350" "XPS 13 DX13261"; do
reset_fixture "20260810-2"
TEST_PRODUCT_NAME="$model" run_leaf
TEST_PRODUCT_NAME="$model" run_migration
[[ ! -s $TEST_LOG ]] || fail "other models receive no firmware repair"
done
reset_fixture "20260810-2"
TEST_INTEL_PTL=0 run_leaf
TEST_INTEL_PTL=0 run_migration
[[ ! -s $TEST_LOG ]] || fail "the Wildcat Lake variant receives no firmware repair"
pass "other models and the Wildcat Lake variant receive no firmware repair"
for version in "" "20260810-2" "20260810-3" "20260810-4" "20260910-2" "20260916-1" "1:20260810-2"; do
reset_fixture "$version"
run_leaf || fail "the alias package is installed during hardware setup"
[[ $(<"$TEST_LOG") == "$install_call" && $(<"$TEST_VERSION_FILE") == "$version" && -e $TEST_ALIAS_FILE ]] ||
fail "hardware setup installs the separate aliases without replacing stock firmware"
done
pass "hardware setup installs the package identity without replacing any stock firmware"
pass "hardware setup leaves reboot bookkeeping to the migration"
reset_fixture "20260810-2"
touch "$TEST_ALIAS_FILE"
run_leaf
[[ ! -s $TEST_LOG ]] || fail "installed aliases are idempotent on stock firmware"
pass "installed aliases are idempotent on stock firmware"
run_migration
[[ $(<"$TEST_LOG") == "state set reboot-required" ]] ||
fail "the migration requests a reboot even if hardware setup already installed the aliases"
pass "the migration requests a reboot even if hardware setup already installed the aliases"
reset_fixture "20260810-2"
if TEST_INSTALL_FAILURE=1 run_migration; then
fail "a failed firmware installation fails the migration"
fi
[[ $(<"$TEST_LOG") == "$install_call" && $(<"$TEST_VERSION_FILE") == "20260810-2" ]] ||
fail "a failed installation leaves the migration retryable without claiming success"
pass "a failed firmware installation fails the migration and remains retryable"
: >"$TEST_LOG"
run_migration
[[ $(<"$TEST_LOG") == "$install_call"$'\nstate set reboot-required' ]] ||
fail "retrying the migration installs the firmware and requests a reboot"
pass "retrying the migration installs the firmware and requests a reboot"
: >"$TEST_LOG"
run_migration
[[ $(<"$TEST_LOG") == "state set reboot-required" ]] ||
fail "a second user before reboot is prompted without reinstalling"
pass "a second user before reboot is prompted without reinstalling"