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:
commit
fdd457c0fa
86 files changed
+2545
-207
No files matched your search
@@ -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"
|
||||
}
|
||||
Executable
+128
@@ -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'
|
||||
Executable
+74
@@ -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
|
||||
@@ -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()
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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.
|
||||
|
||||
@@ -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"
|
||||
@@ -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,
|
||||
|
||||
@@ -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
|
||||
@@ -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")"
|
||||
|
||||
@@ -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"
|
||||
@@ -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"
|
||||
|
||||
@@ -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 ||
|
||||
|
||||
@@ -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"
|
||||
@@ -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
|
||||
|
||||
Executable
+127
@@ -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"
|
||||
Reference in new issue
Block a user