Merge branch 'quattro' into unified-input-method-setup

This commit is contained in:
David Heinemeier Hansson committed 2026-10-10 15:51:52 +02:00
commit 101bb547bb
245 files changed
+19949 -375

No files matched your search

+93
View File
@@ -0,0 +1,93 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# PC and Intel Mac quirks that must leave Apple Silicon alone, run on both sides
# of the detector.
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls.log"
conf="$test_tmp/modprobe.d/hid_apple.conf"
mkdir -p "$stub_bin"
cat >"$stub_bin/omarchy-hw-aarch64-apple" <<'SH'
#!/bin/bash
[[ ${APPLE_SILICON:-0} == "1" ]]
SH
cat >"$stub_bin/uname" <<'SH'
#!/bin/bash
if [[ $1 == "-m" ]]; then
echo "${UNAME_M:-x86_64}"
else
exec /usr/bin/uname "$@"
fi
SH
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo %s\n' "$*" >>"$TEST_LOG"
"$@"
SH
chmod +x "$stub_bin"/*
run_fkeys() {
APPLE_SILICON="$1" OMARCHY_HID_APPLE_CONF="$conf" PATH="$stub_bin:$PATH" TEST_LOG="$calls" \
bash -eE -o pipefail -c 'source "$1"' _ "$ROOT/install/hardware/fix-fkeys.sh"
}
run_fkeys 0
[[ $(<"$conf") == "options hid_apple fnmode=2" ]] || fail "a PC keeps F-keys first on Apple-like keyboards" "$(cat "$conf")"
pass "a PC keeps F-keys first on Apple-like keyboards"
printf 'options hid_apple fnmode=0\n' >"$conf"
run_fkeys 0
[[ $(<"$conf") == "options hid_apple fnmode=0" ]] || fail "an existing hid_apple.conf is left alone"
pass "an existing hid_apple.conf is left alone"
rm -rf "$test_tmp/modprobe.d"
: >"$calls"
run_fkeys 1
[[ ! -e $conf && ! -s $calls ]] || fail "Apple Silicon keeps its own keyboard mode" "$(cat "$calls")"
pass "Apple Silicon keeps its own keyboard mode"
# The Windows guest is x86_64, so every other CPU refuses it before it touches
# anything, Apple Silicon or not.
for machine in aarch64 armv7l; do
output=$(UNAME_M=$machine APPLE_SILICON=0 PATH="$stub_bin:$ROOT/bin:$PATH" bash "$ROOT/bin/omarchy-windows-vm" install 2>&1) &&
fail "Windows VM install fails on $machine"
[[ $output == *"needs an x86_64 machine"* ]] || fail "Windows VM says why it refuses on $machine" "$output"
done
pass "Windows VM refuses every CPU but x86_64"
# The firmware boot entry and hibernation are refused or skipped on Apple
# Silicon before they touch anything.
output=$(APPLE_SILICON=1 PATH="$stub_bin:$PATH" bash "$ROOT/bin/omarchy-setup-direct-boot" 2>&1) &&
fail "direct boot fails on Apple Silicon"
[[ $output == *"not supported on Apple Silicon"* ]] || fail "direct boot says why it refuses" "$output"
output=$(APPLE_SILICON=1 PATH="$stub_bin:$PATH" bash "$ROOT/bin/omarchy-hibernation-setup" --force 2>&1) ||
fail "hibernation setup skips Apple Silicon without failing" "$output"
[[ $output == "Skipping hibernation setup (not supported on Apple Silicon)" ]] ||
fail "hibernation setup stops before its own checks on Apple Silicon" "$output"
pass "direct boot and hibernation step aside on Apple Silicon"
# On x86_64 the Windows VM goes on to its own commands, as before.
output=$(UNAME_M=x86_64 PATH="$stub_bin:$ROOT/bin:$PATH" bash "$ROOT/bin/omarchy-windows-vm" help 2>&1) || true
[[ $output != *"x86_64 machine"* && $output == *"install"* ]] || fail "Windows VM runs as before on x86_64" "$output"
pass "Windows VM runs as before on x86_64"
run_node_test <<'JS'
const fs = require('fs')
const menu = requireFromRoot('shell/plugins/menu/MenuModel.js')
const items = menu.parseMenuJsonc(fs.readFileSync(path.join(root, 'default/omarchy/omarchy-menu.jsonc'), 'utf8'))
const when = id => (items.find(entry => entry.id === id) || {}).when
assertEqual(when('install.windows'), 'omarchy-hw-x86', 'install.windows is offered on x86_64 only')
assertEqual(when('setup.direct-boot'), '! omarchy-hw-aarch64-apple', 'setup.direct-boot is hidden on Apple Silicon only')
JS
pass "the menu offers Windows on x86_64 only and hides Direct Boot on Apple Silicon only"
@@ -0,0 +1,76 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/bin"
cat >"$work/bin/wpctl" <<'SH'
#!/bin/bash
printf 'wpctl %s\n' "$*" >>"$CALLS"
SH
cat >"$work/bin/pactl" <<'SH'
#!/bin/bash
printf 'pactl %s\n' "$*" >>"$CALLS"
if [[ $* == "list source-outputs" ]]; then
[[ ${LC_ALL:-} == C ]] || echo "pactl read outside the C locale" >>"$CALLS"
cat "$OUTPUTS"
fi
exit 0
SH
chmod +x "$work/bin/wpctl" "$work/bin/pactl"
moved() {
grep '^pactl move-source-output ' "$CALLS" | awk '{ print $3 }' | tr '\n' ' '
}
export CALLS="$work/calls" OUTPUTS="$work/outputs"
# A filter chain's capture half (joined to its source by node.link-group)
# records its device all the time. Choosing an input moves apps, never it.
cat >"$OUTPUTS" <<'OUT'
Source Output #67
Driver: PipeWire
Properties:
node.name = "filter_capture.mic"
node.group = "filter-chain-100-18"
node.link-group = "filter-chain-100-18"
media.name = "Microphone filter"
Source Output #90
Driver: PipeWire
Properties:
application.name = "Firefox"
node.name = "Firefox"
Source Output #91
Properties:
node.name = "native-recorder"
media.name = "node.link-group = in a title"
OUT
: >"$CALLS"
PATH="$work/bin:$PATH" "$ROOT/bin/omarchy-audio-input-set-default" 113 virtual_mic
grep -qx 'wpctl set-default 113' "$CALLS" || fail 'input set-default selects the node'
grep -qx 'pactl set-default-source virtual_mic' "$CALLS" || fail 'input set-default names the default source'
! grep -q 'outside the C locale' "$CALLS" || fail 'input set-default reads the recordings in the C locale'
[[ $(moved) == "90 91 " ]] || fail 'input set-default moves apps but not a filter'"'"'s own capture' "$(moved)"
pass 'input set-default moves apps but not a filter'"'"'s own capture'
# Without filters every recording moves, as before, whatever its properties.
cat >"$OUTPUTS" <<'OUT'
Source Output #12
Properties:
application.name = "OBS"
Source Output #13
Properties:
node.name = "native-recorder"
Source Output #14
Properties:
node.name = "filter_capture.mic-copy"
node.group = "pipewire.dummy"
OUT
: >"$CALLS"
PATH="$work/bin:$PATH" "$ROOT/bin/omarchy-audio-input-set-default" 43 alsa_input.pci-0000_00_1f.3.analog-stereo
[[ $(moved) == "12 13 14 " ]] || fail 'input set-default still moves every other recording' "$(moved)"
pass 'input set-default still moves every other recording'
@@ -0,0 +1,65 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir -p "$work/bin"
cat >"$work/bin/pactl" <<'SH'
#!/bin/bash
printf '%s %s\n' "${LC_ALL:-unset}" "$*" >>"$CALLS"
case $* in
"list sources") cat "$SOURCES" ;;
"list sinks") cat "$SINKS" ;;
esac
SH
cat >"$work/bin/omarchy-audio-tuning" <<'SH'
#!/bin/bash
exit 1
SH
chmod +x "$work/bin/pactl" "$work/bin/omarchy-audio-tuning"
export CALLS="$work/calls" SOURCES="$work/sources" SINKS="$work/sinks"
# A headset jack input with nothing plugged in, a virtual source (no ports)
# and a filter's source (one port of unknown availability).
cat >"$SOURCES" <<'OUT'
Source #1074
Name: alsa_input.pci-0000_00_1f.3.analog-stereo
Ports:
[In] analog-input-headset-mic: Headset Microphone (type: Headset, priority: 300, availability group: Headset Mic, not available)
Active Port: [In] analog-input-headset-mic
Formats:
pcm
Source #126
Name: easyeffects_source
Formats:
pcm
Source #1102
Name: effect_output.mic-filter
Ports:
[In] Mic: Microphone (type: Mic, priority: 100, availability unknown)
Active Port: [In] Mic
OUT
cat >"$SINKS" <<'OUT'
Sink #57
Name: alsa_output.pci-0000_00_1f.3.analog-stereo
Ports:
[Out] analog-output-headphones: Headphones (type: Headphones, priority: 300, availability group: Headphone, not available)
Active Port: [Out] analog-output-headphones
OUT
: >"$CALLS"
expected=$'alsa_input.pci-0000_00_1f.3.analog-stereo\t0\neasyeffects_source\t1\neffect_output.mic-filter\t1'
actual=$(PATH="$work/bin:$PATH" "$ROOT/bin/omarchy-audio-sink-availability" sources)
[[ $actual == "$expected" ]] || fail 'source availability reports an empty jack input unavailable' "$actual"
grep -qx 'C list sources' "$CALLS" || fail 'source availability reads pactl in the C locale' "$(cat "$CALLS")"
pass 'source availability reports an empty jack input unavailable'
: >"$CALLS"
actual=$(LC_ALL=de_DE.UTF-8 PATH="$work/bin:$PATH" "$ROOT/bin/omarchy-audio-sink-availability")
[[ $actual == $'alsa_output.pci-0000_00_1f.3.analog-stereo\t0' ]] || fail 'sink availability reports an empty headphone jack unavailable' "$actual"
grep -qx 'C list sinks' "$CALLS" || fail 'sink availability reads pactl in the C locale whatever the caller'"'"'s' "$(cat "$CALLS")"
pass 'sink availability reports an empty headphone jack unavailable, in the C locale'
+68
View File
@@ -57,4 +57,72 @@ assertEqual(audio.matchingMprisStreamLabel('Chromium', players), 'Chromium', 'au
assertEqual(audio.unmatchedMprisStreamLabel('audio-src', players, streams), 'Spotify', 'audio uses unmatched MPRIS player for generic streams')
assertEqual(audio.streamLabel(streams[1], players, streams), 'Spotify', 'audio labels generic streams from MPRIS')
assert(audio.streamRepresentsPlayer(streams[1], players[0], players, streams), 'audio links generic streams to active player')
// A virtual source is untyped in Quickshell (no audio); PulseAudio's source
// listing is what names it a source.
const availability = { virtual_mic: true, 'alsa_input.usb': false }
assert(audio.isUntypedSource({ name: 'virtual_mic', isSink: false, isStream: false }, availability), 'audio finds an untyped source PulseAudio lists')
assert(audio.isUntypedSource({ name: 'unplugged_virtual', isSink: false, isStream: false }, { unplugged_virtual: false }), 'an untyped source PulseAudio lists as unavailable is still a source')
assert(!audio.isUntypedSource({ name: 'v4l2_input.camera', isSink: false, isStream: false }, availability), 'audio skips an untyped node PulseAudio does not list')
assert(!audio.isUntypedSource({ name: 'virtual_mic', audio: {}, isSink: false, isStream: false }, availability), 'a typed source is not untyped')
assert(!audio.isUntypedSource({ name: 'virtual_mic', isSink: false, isStream: true }, availability), 'a stream is never an untyped source')
assert(!audio.isUntypedSource({ name: '', isSink: false, isStream: false }, { '': true }), 'a nameless node is never an untyped source')
assert(!audio.isUntypedSource({ name: 'virtual_mic', isSink: false, isStream: false }, {}), 'nothing is untyped before the listing')
const nodes = requireFromRoot('shell/Commons/AudioNodesModel.js')
for (const name of ['quickshell', 'quickshell-peak-monitor'])
assert(nodes.isShellLevelMeter(name), 'audio knows the shell meter ' + name)
for (const name of ['Firefox', 'quickshell-other', '', undefined])
assert(!nodes.isShellLevelMeter(name), 'audio counts ' + name + ' as a recording')
// Platform hints: none by default, whole-name patterns, and "replaced" only
// while its replacement exists.
const none = nodes.parsePlatformAudio('')
assertDeepEqual([none.hidden.length, none.replaced.length], [0, 0], 'audio has no platform hints without the file')
for (const text of ['not json', '[]', 'null', '{"hidden": "x"}', '{"replaced": [1, {"node": "a"}, {"node": "(", "by": "b"}]}']) {
const parsed = nodes.parsePlatformAudio(text)
assertEqual(parsed.hidden.length + parsed.replaced.length, 0, 'audio ignores platform hints ' + text)
}
const hints = nodes.parsePlatformAudio(JSON.stringify({
hidden: ['dsp_capture\\.[a-z]+', 'raw_speakers', '(', 7],
replaced: [{ node: 'dsp_mic\\.[0-9]+', by: 'stereo_mic' }]
}))
assertEqual(hints.hidden.length, 2, 'audio skips invalid hidden patterns')
const names = ['dsp_capture.mic', 'dsp_mic.1', 'stereo_mic', 'raw_speakers']
assert(nodes.platformHidesNode('dsp_capture.mic', hints, names), 'platform hints hide a matching node')
assert(nodes.platformHidesNode('raw_speakers', hints, []), 'platform hints hide a literal name')
assert(!nodes.platformHidesNode('raw_speakers.monitor', hints, []), 'platform patterns match whole names only')
assert(!nodes.platformHidesNode('xdsp_capture.mic', hints, []), 'platform patterns are anchored at the start')
assert(nodes.platformHidesNode('dsp_mic.1', hints, names), 'platform hints hide a replaced node while its replacement exists')
assert(!nodes.platformHidesNode('dsp_mic.1', hints, ['dsp_mic.1']), 'platform hints keep a replaced node without its replacement')
assert(!nodes.platformHidesNode('stereo_mic', hints, names), 'platform hints keep the replacement')
for (const name of ['Firefox', 'alsa_input.pci-0000_00_1f.3.analog-stereo', '', undefined])
assert(!nodes.platformHidesNode(name, hints, names), 'platform hints keep ' + name)
assert(!nodes.platformHidesNode('dsp_capture.mic', none, names), 'no platform hints hide nothing')
// An untyped source's level is read from wpctl.
assertDeepEqual(nodes.parseWpctlVolume('Volume: 0.50\n'), { volume: 0.5, muted: false }, 'audio reads a wpctl volume')
assertDeepEqual(nodes.parseWpctlVolume('Volume: 1.00 [MUTED]'), { volume: 1, muted: true }, 'audio reads a muted wpctl volume')
for (const text of ['', 'Translate ID error: 404', 'Volume: loud', 'Volume: 0.50 [muted]'])
assertEqual(nodes.parseWpctlVolume(text), null, 'audio rejects wpctl output ' + JSON.stringify(text))
// Source wiring (no Quickshell here): the hints come from the fixed platform
// root, and the audio code names no platform or hardware of its own.
const fs = require('fs')
const read = (file) => fs.readFileSync(path.join(root, file), 'utf8')
const audioNodes = read('shell/Commons/AudioNodes.qml')
assert(/path: "\/usr\/share\/omarchy-platform\/audio\.json"/.test(audioNodes) && !/Quickshell\.env|OMARCHY_/.test(audioNodes),
'audio reads the platform hints from the fixed platform root')
assert(/onLoadFailed: missing = true/.test(audioNodes), 'a removed hints file hides nothing')
const sources = ['shell/plugins/panels/audio/Panel.qml', 'shell/plugins/panels/audio/Model.js', 'shell/Commons/AudioNodes.qml',
'shell/Commons/AudioNodesModel.js', 'shell/Commons/UntypedInput.qml', 'shell/plugins/bar/widgets/Microphone.qml',
'bin/omarchy-audio-input-set-default', 'bin/omarchy-audio-sink-availability']
for (const file of sources)
assert(!/apple|asahi|macbook|j[0-9]{3}|omarchy-hw-|platform-sound/i.test(read(file)), file + ' names no platform')
const panel = read('shell/plugins/panels/audio/Panel.qml')
assert(/inputPeakNode: inputViaWpctl \? null : source/.test(panel) && /inputLevelShown: !!inputPeakNode/.test(panel) &&
/visible: root\.inputLevelShown[^}]*inputPeakMonitor\.peak/.test(panel),
'the input level bar is hidden when the input is driven through wpctl')
JS
+235
View File
@@ -0,0 +1,235 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
run_node_test <<'JS'
const fs = require('fs')
const bar = requireFromRoot('shell/plugins/bar/BarModel.js')
const barSource = fs.readFileSync(root + '/shell/plugins/bar/Bar.qml', 'utf8')
const styleSource = fs.readFileSync(root + '/shell/Commons/Style.qml', 'utf8')
const shellJson = fs.readFileSync(root + '/config/omarchy/shell.json', 'utf8')
// A platform package's cutout description, as it would ship it.
const cutouts = bar.parseCutouts(JSON.stringify({ panels: [
{ connector: 'eDP', width: 3024, height: 1964, top: 64 },
{ connector: 'eDP', width: 2560, height: 1664, top: 56 }
] }))
assertEqual(cutouts.length, 2, 'a well-formed description yields its panels')
// Malformed descriptions and entries are dropped, never guessed at.
for (const text of ['', 'not json', '[]', '{}', '{"panels": {}}'])
assertDeepEqual(bar.parseCutouts(text), [], `an unusable description (${JSON.stringify(text)}) yields no cutouts`)
assertDeepEqual(bar.parseCutouts(JSON.stringify({ panels: [
{ width: 3024, height: 1964, top: 64 },
{ connector: '', width: 3024, height: 1964, top: 64 },
{ connector: 'eDP', width: 0, height: 1964, top: 64 },
{ connector: 'eDP', width: 3024, height: 1964, top: 0 },
{ connector: 'eDP', width: 3024, height: 1964, top: 1964 },
null
] })), [], 'entries without a connector, a size or a sensible cutout are dropped')
// A cutout is described in physical rows; the bar works in logical pixels at
// any scale. Hyprland reports each output's physical mode, as hyprctl monitors
// does; Qt's devicePixelRatio is a whole number even at a fractional scale.
const mode = (width, height, transform = 0) => ({ width, height, transform })
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1512, 982, 2, mode(3024, 1964)), 32, 'a 64-row cutout is 32 px at scale 2')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 3024, 1964, 1, mode(3024, 1964)), 64, 'scale 1 uses the rows as they are')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1280, 832, 2, mode(2560, 1664)), 28, 'each described panel gets its own cutout')
// A 16" MacBook Pro panel at scale 1.33: Qt says 2, Hyprland says 3456x2234.
const pro16 = [{ connector: 'eDP', width: 3456, height: 2234, top: 64 }]
assertEqual(bar.cutoutTop(pro16, 'eDP-1', 2592, 1676, 2, mode(3456, 2234)), 48, 'a fractional scale matches its panel by the mode Hyprland reports')
assertEqual(bar.notchFloor(pro16, 'top', 'eDP-1', 2592, 1676, 2, mode(3456, 2234), 0), 48, 'a top bar at a fractional scale floors at the cutout')
assertEqual(bar.centerBesideRight(pro16, 'top', 'eDP-1', 2592, 1676, 2, mode(3456, 2234)), true, 'a top bar at a fractional scale moves the center section')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1890, 1228, 2, mode(3024, 1964)), 40, 'scale 1.6 matches too')
// Until Hyprland answers, the logical size times Qt's ratio stands in for the
// mode, which holds at whole scales only.
for (const pending of [null, undefined, {}, mode(0, 0)])
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1512, 982, 2, pending), 32, `a whole scale matches before Hyprland reports the mode (${JSON.stringify(pending)})`)
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 3024, 1964, 0, null), 64, 'a missing scale reads as 1')
assertEqual(bar.cutoutTop(pro16, 'eDP-1', 2592, 1676, 2, null), 0, 'a fractional scale finds nothing on Qt\'s ratio alone')
// A mode Hyprland reports is the answer: one no panel describes has no cutout,
// whatever the logical size times the ratio would say.
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1512, 982, 2, mode(3024, 1890)), 0, 'the same panel with its cutout rows hidden has none')
assertEqual(bar.cutoutTop(cutouts, 'DP-1', 1512, 982, 2, mode(3024, 1964)), 0, 'another connector with the same mode has none')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1728, 1117, 2, mode(3456, 2234)), 0, 'a panel nobody described has none')
assertEqual(bar.cutoutTop([], 'eDP-1', 1512, 982, 2, mode(3024, 1964)), 0, 'with no description no panel has a cutout')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 0, 0, 2, mode(3024, 1964)), 0, 'degenerate screen sizes have none')
// Turned a quarter or upside down, the cutout is on another edge; mirrored,
// it is still at the top.
for (const transform of [1, 2, 3, 5, 6, 7])
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', transform % 2 ? 982 : 1512, transform % 2 ? 1512 : 982, 2, mode(3024, 1964, transform)), 0, `a panel with transform ${transform} has no cutout at its top`)
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 1512, 982, 2, mode(3024, 1964, 4)), 32, 'a mirrored panel keeps its cutout at the top')
assertEqual(bar.cutoutTop(cutouts, 'eDP-1', 982, 1512, 2, mode(3024, 1964)), 0, 'a portrait screen on a landscape mode is turned, whatever its transform says')
// Only a top bar on a described panel has a floor; a calibrated [bar]
// notch-height replaces the described value there and nowhere else.
assertEqual(bar.notchFloor(cutouts, 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964), 0), 32, 'a top bar floors at the cutout')
for (const position of ['bottom', 'left', 'right'])
assertEqual(bar.notchFloor(cutouts, position, 'eDP-1', 1512, 982, 2, mode(3024, 1964), 0), 0, `a ${position} bar has no floor`)
assertEqual(bar.notchFloor(cutouts, 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964), 36), 36, 'a calibrated notch-height overrides the described floor')
assertEqual(bar.notchFloor(cutouts, 'top', 'DP-1', 2560, 1440, 1, mode(2560, 1440), 36), 0, 'a calibrated notch-height never floors an external monitor')
assertEqual(bar.notchFloor([], 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964), 36), 0, 'a calibrated notch-height never floors a panel without a cutout')
assertEqual(bar.notchFloor(cutouts, 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964), 'x'), 32, 'an unreadable calibration falls back to the described floor')
// Source wiring only (no Quickshell here): the bar reads the description from
// the fixed platform root, which no environment variable moves, not from a
// detector.
assert(
/path: "\/usr\/share\/omarchy-platform\/display-cutouts\.json"/.test(barSource) &&
/readonly property var displayCutouts: displayCutoutsFile\.missing \? \[\] : BarModel\.parseCutouts\(displayCutoutsFile\.text\(\)\)/.test(barSource),
'bar reads the platform package\'s cutout description from the platform root'
)
assert(
/id: displayCutoutsFile[\s\S]{0,400}blockLoading: true[\s\S]{0,200}onLoadFailed: missing = true/.test(barSource),
'bar reads the description as the shell starts, before its surfaces map, and a removed file describes nothing'
)
assert(!/_PACKAGED_PATH|packagedPath/.test(barSource), 'no environment variable moves the cutout description')
assert(!/omarchy-hw-/.test(barSource), 'bar asks no hardware detector')
assert(
/BarModel\.notchFloor\(root\.displayCutouts, root\.position, screen\.name, screen\.width, screen\.height, screen\.devicePixelRatio, root\.panelModeFor\(screen\), Style\.bar\.notchHeight\)/.test(barSource),
'bar derives the floor from its own screen geometry, its mode and the calibration'
)
assert(
/function panelModeFor\(screen\) \{\s*var monitor = screen \? Hyprland\.monitorFor\(screen\) : null\s*return monitor \? \(\{\s*width: monitor\.width,\s*height: monitor\.height,\s*transform: monitor\.lastIpcObject \? monitor\.lastIpcObject\.transform : 0\s*\}\) : null/.test(barSource) &&
/readonly property var panelMode: root\.panelModeFor\(screen\)/.test(barSource),
'bar takes its screen\'s physical mode from Hyprland'
)
assert(
/function thicknessFor\(screen\) \{\s*if \(root\.vertical \|\| !screen\) return root\.barSize\s*return Math\.max\(root\.barSize, BarModel\.notchFloor\(/.test(barSource) &&
/readonly property int thickness: root\.thicknessFor\(screen\)/.test(barSource) &&
/implicitHeight: root\.vertical \? 0 : thickness/.test(barSource),
'bar height is floored at the cutout, never shrunk to it'
)
// A hidden bar parks by that same thickness (bar-test.sh), so a floored top
// bar leaves no strip of itself on screen.
// The calibration must not scale with the font: it describes physical pixels
// beside the camera.
assert(
/notchHeight:[\s\S]{0,240}barOverrides\["notch-height"\]/.test(styleSource) &&
!/barToken\("notch-height"/.test(styleSource),
'notch-height is read raw, not through the font-scaled bar tokens'
)
// A top bar on a described panel draws its center section beside the right
// one; everything else keeps the configured layout.
for (const scale of [1, 2])
assertEqual(bar.centerBesideRight(cutouts, 'top', 'eDP-1', 3024 / scale, 1964 / scale, scale, mode(3024, 1964)), true, `a described panel at scale ${scale} moves the center section`)
assertEqual(bar.centerBesideRight(cutouts, 'top', 'USB-2', 3440, 1440, 1, mode(3440, 1440)), false, 'an external monitor keeps the center section')
assertEqual(bar.centerBesideRight([], 'top', 'eDP-1', 1512, 982, 2, mode(3024, 1964)), false, 'a machine without a description keeps the center section')
for (const position of ['bottom', 'left', 'right'])
assertEqual(bar.centerBesideRight(cutouts, position, 'eDP-1', 1512, 982, 2, mode(3024, 1964)), false, `a ${position} bar keeps the center section`)
// The move is per screen and draw-time only: the user's layout is read as
// configured, and the center entries keep their order and their own region.
assert(
/centerBesideRight: BarModel\.centerBesideRight\(root\.displayCutouts, root\.position, screen\.name, screen\.width, screen\.height, screen\.devicePixelRatio, panelMode\)/.test(barSource),
'each bar surface decides the move from its own screen'
)
assert(
/CenterModules \{\s*anchors\.fill: parent\s*entries: barWindow\.centerBesideRight \? \[\] : root\.layoutEntries\("center"\)\s*\}/.test(barSource),
'a moved center section draws nothing in the middle of the bar'
)
assert(
/ModuleList \{\s*entries: barWindow\.centerBesideRight \? root\.layoutEntries\("center"\) : \[\]\s*region: "center"\s*anchors\.right: rightModules\.left\s*anchors\.rightMargin: Style\.space\(\d+\)/.test(barSource),
'the center entries sit just left of the right section in their own region'
)
// With no gap the last center slot and the first right slot would share an
// edge, and a drop there would always land in whichever registered first.
const seam = [
{ slot: 'right-first', x: 104, width: 20 },
{ slot: 'center-last', x: 80, width: 20 }
]
assertDeepEqual(bar.nearestDropTarget(seam, { x: 101 }, false), { slot: 'center-last', after: true }, 'a drop at the seam nearer the center section lands after its last entry')
assertDeepEqual(bar.nearestDropTarget(seam, { x: 103 }, false), { slot: 'right-first', after: false }, 'a drop at the seam nearer the right section lands before its first entry')
assert(
/anchorEntry: root\.findCenterAnchorEntry\(entries\)/.test(barSource),
'an emptied center section does not keep a hidden copy of its anchor'
)
// Until Hyprland reports the mode, a top bar waits to map where the mode may
// still bring a cutout: a described connector whose whole-scale rebuild
// matched nothing, as at a fractional scale. It never guesses a floor there.
assertEqual(bar.cutoutPending(pro16, 'top', 'eDP-1', 2592, 1676, 2, null), true, 'a fractional scale on a described connector waits for the mode')
for (const pending of [undefined, {}, mode(0, 0)])
assertEqual(bar.cutoutPending(pro16, 'top', 'eDP-1', 2592, 1676, 2, pending), true, `no mode yet (${JSON.stringify(pending)}) still waits`)
assertEqual(bar.notchFloor(pro16, 'top', 'eDP-1', 2592, 1676, 2, null, 0), 0, 'a waiting bar has no guessed floor')
assertEqual(bar.cutoutPending(pro16, 'top', 'eDP-1', 2592, 1676, 2, mode(3456, 2234)), false, 'the mode ends the wait')
assertEqual(bar.cutoutPending(pro16, 'top', 'eDP-1', 1728, 1117, 2, mode(3456, 2160)), false, 'a mode no panel describes ends the wait too')
assertEqual(bar.cutoutPending(cutouts, 'top', 'eDP-1', 1512, 982, 2, null), false, 'a whole scale that matches needs no wait')
assertEqual(bar.cutoutPending(cutouts, 'top', 'DP-1', 2560, 1440, 1, null), false, 'a connector no entry names never waits')
assertEqual(bar.cutoutPending([], 'top', 'eDP-1', 2592, 1676, 2, null), false, 'without a description nothing waits')
for (const position of ['bottom', 'left', 'right'])
assertEqual(bar.cutoutPending(pro16, position, 'eDP-1', 2592, 1676, 2, null), false, `a ${position} bar never waits`)
assert(
/readonly property bool cutoutPending: BarModel\.cutoutPending\(root\.displayCutouts, root\.position, screen\.name, screen\.width, screen\.height, screen\.devicePixelRatio, panelMode\)/.test(barSource) &&
/visible: !remapGuard\.remapping && \(cutoutWaitOver \|\| !cutoutPending\)/.test(barSource),
'a bar surface maps only once its floor is known, and still remaps on a screen move'
)
assert(
/property bool cutoutWaitOver: false\s*onCutoutPendingChanged: if \(!cutoutPending\) cutoutWaitOver = true\s*Component\.onCompleted: if \(!cutoutPending\) cutoutWaitOver = true/.test(barSource) &&
/Timer \{\s*interval: 2000\s*running: barWindow\.cutoutPending && !barWindow\.cutoutWaitOver\s*onTriggered: barWindow\.cutoutWaitOver = true\s*\}/.test(barSource),
'the wait ends once, for good: when the mode settles it or after two seconds'
)
// Toasts and plugins clear the bar as thick as it is on their screen. The bar
// publishes each screen's thickness by name from the same calculation its
// surfaces use; barSize stays the configured size the widgets are drawn at.
const sizes = { 'eDP-1': 40, 'DP-1': 26 }
assertEqual(bar.barSizeFor(sizes, 'eDP-1', 26), 40, 'a notch screen reports its floored thickness')
assertEqual(bar.barSizeFor(sizes, 'DP-1', 26), 26, 'another screen reports its own')
assertEqual(bar.barSizeFor(sizes, 'HDMI-A-1', 26), 26, 'a screen without a surface falls back to the configured size')
assertEqual(bar.barSizeFor({}, 'eDP-1', 26), 26, 'no published sizes fall back to the configured size')
assertEqual(bar.barSizeFor(sizes, 'constructor', 26), 26, 'inherited object keys are no screen')
assert(
/readonly property var screenBarSizes: \{\s*var sizes = \(\{\}\)\s*var screens = Quickshell\.screens\s*for \(var i = 0; i < screens\.length; i\+\+\) sizes\[screens\[i\]\.name\] = root\.thicknessFor\(screens\[i\]\)\s*return sizes\s*\}/.test(barSource) &&
/function barSizeFor\(screenName\) \{\s*return BarModel\.barSizeFor\(root\.screenBarSizes, screenName, root\.barSize\)\s*\}/.test(barSource),
'bar publishes every screen\'s thickness from the same calculation its surfaces use'
)
assert(
/readonly property int barSize: vertical \? Style\.bar\.sizeVertical : Style\.bar\.sizeHorizontal/.test(barSource),
'barSize stays the configured size'
)
// The plugin facade copies the sizes, holding no reference into the bar, and
// keeps barSize the configured size.
const shellSource = fs.readFileSync(root + '/shell/shell.qml', 'utf8')
const stateApiSource = fs.readFileSync(root + '/shell/services/PluginBarStateApi.qml', 'utf8')
const extract = (source, name) => {
const match = source.match(new RegExp(`function ${name}\\((\\w+)\\) \\{([\\s\\S]*?)\\n \\}\\n`))
assert(match, `${name} is defined`)
return match
}
const detached = extract(shellSource, 'detachedBarSizes')
const Util = { isPlainObject: value => value !== null && typeof value === 'object' && !Array.isArray(value) }
const detachedBarSizes = new Function('Util', detached[1], detached[2])
const live = { 'eDP-1': 40, 'DP-1': 26, broken: 'x', none: 0 }
const copy = detachedBarSizes.call(null, Util, live)
assertDeepEqual(copy, { 'eDP-1': 40, 'DP-1': 26 }, 'the plugin facade copies each screen\'s thickness')
assert(copy !== live, 'the copy is detached from the bar\'s map')
assertDeepEqual(detachedBarSizes.call(null, Util, undefined), {}, 'a bar without per-screen sizes gives an empty map')
assert(
/api\.barSize = Qt\.binding\(function\(\) \{ return shell\.bar \? Math\.max\(0, shell\.bar\.barSize \|\| 0\) : 0 \}\)/.test(shellSource) &&
/api\.barSizes = Qt\.binding\(function\(\) \{ return shell\.bar \? shell\.detachedBarSizes\(shell\.bar\.screenBarSizes\) : \(\{\}\) \}\)/.test(shellSource),
'plugins get the configured barSize and every screen\'s thickness'
)
const apiFor = extract(stateApiSource, 'barSizeFor')
const stateBarSizeFor = new Function('barSizes', 'barSize', apiFor[1], apiFor[2])
assertEqual(stateBarSizeFor(copy, 26, 'eDP-1'), 40, 'a plugin reads a notch screen\'s thickness')
assertEqual(stateBarSizeFor(copy, 26, 'HDMI-A-1'), 26, 'a plugin falls back to barSize for a screen with none')
assertEqual(stateBarSizeFor({}, 26, 'eDP-1'), 26, 'a plugin under a bar without per-screen sizes gets barSize')
const parsed = JSON.parse(shellJson)
assertEqual(parsed.bar.centerAnchor, 'omarchy.clock', 'shipped bar layout still anchors on the clock')
assert(
JSON.stringify(parsed.bar.layout.center).indexOf('omarchy.indicators') >= 0,
'shipped bar layout still keeps the default center widgets'
)
JS
+2 -2
View File
@@ -81,8 +81,8 @@ assert(
)
for (const edge of ['top', 'bottom', 'left', 'right']) {
assert(
new RegExp(`${edge}: root\\.barHidden && root\\.position === "${edge}" \\? -root\\.barSize : 0`).test(barSource),
`a hidden bar parks past the ${edge} edge`
new RegExp(`${edge}: root\\.barHidden && root\\.position === "${edge}" \\? -barWindow\\.thickness : 0`).test(barSource),
`a hidden bar parks past the ${edge} edge by its whole thickness`
)
}
+52
View File
@@ -89,6 +89,58 @@ require_compositor() {
exit 0
}
# Fake the hardware identity omarchy-hw-platform reads: a device tree under
# $1/proc and a uname in $1/bin reporting the platform's CPU. Run the code under
# test with OMARCHY_PROC_ROOT="$1/proc" and PATH="$1/bin:$ROOT/bin:...", so the
# real detector and its wrappers answer from the fixture.
fake_platform() {
local dir="$1" platform="$2" machine=aarch64
local -a compatible=()
case $platform in
aarch64-apple) compatible=(apple,j416c apple,t6021 apple,arm-platform) ;;
aarch64) compatible=(raspberrypi,5-model-b brcm,bcm2712) ;;
x86) machine=x86_64 ;;
*) fail "fake_platform knows the platform $platform" ;;
esac
rm -rf "$dir/proc"
mkdir -p "$dir/proc" "$dir/bin"
if (( ${#compatible[@]} > 0 )); then
mkdir -p "$dir/proc/device-tree"
printf '%s\0' "${compatible[@]}" >"$dir/proc/device-tree/compatible"
fi
cat >"$dir/bin/uname" <<SH
#!/bin/bash
[[ \${1:-} == -m ]] && { printf '%s\\n' $machine; exit 0; }
exec /usr/bin/uname "\$@"
SH
chmod +x "$dir/bin/uname"
}
# omarchy-hw-platform ignores fixture roots when it runs as root, so a root run
# cannot fake a platform. Skip the rest of the file there.
require_platform_fixtures() {
(( EUID != 0 )) && return 0
skip "running as root, where omarchy-hw-platform ignores fixtures; skipping $1"
exit 0
}
# A script reads the installed platform package's files from the fixed
# /usr/share/omarchy-platform, which nothing in its environment moves. Copies
# script $1 to $2 reading fixture root $3 instead (a directory that doesn't
# exist stands for no platform package), the way the security tests rewrite
# other fixed paths.
platform_root_copy() {
local script="$1" copy="$2" fixture="$3"
sed -e "s|/usr/share/omarchy-platform|$fixture|g" "$script" >"$copy" &&
chmod +x "$copy" || fail "copy $script for a fixture platform root"
grep -qF -- "$fixture" "$copy" && ! grep -qF /usr/share/omarchy-platform "$copy" ||
fail "$(basename -- "$script") reads the platform root from the fixture"
}
run_node_test() {
require_command node
+38
View File
@@ -0,0 +1,38 @@
#!/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/macsmc-ac" "$test_tmp/CMB0"
printf 'Mains\n' >"$test_tmp/macsmc-ac/type"
printf 'Battery\n' >"$test_tmp/CMB0/type"
printf '1\n' >"$test_tmp/CMB0/present"
OMARCHY_POWER_SUPPLY_PATH="$test_tmp" "$ROOT/bin/omarchy-battery-present" || fail "battery presence accepts non-BAT native paths"
pass "battery presence accepts non-BAT native paths"
printf '0\n' >"$test_tmp/CMB0/present"
if OMARCHY_POWER_SUPPLY_PATH="$test_tmp" "$ROOT/bin/omarchy-battery-present"; then
fail "battery presence rejects an absent battery"
fi
pass "battery presence rejects an absent battery"
# A wireless mouse registers a Battery of its own, scoped Device; it must not
# make a desktop look like a laptop.
printf '1\n' >"$test_tmp/CMB0/present"
printf 'System\n' >"$test_tmp/CMB0/scope"
mkdir -p "$test_tmp/hidpp_battery_0"
printf 'Battery\n' >"$test_tmp/hidpp_battery_0/type"
printf '1\n' >"$test_tmp/hidpp_battery_0/present"
printf 'Device\n' >"$test_tmp/hidpp_battery_0/scope"
OMARCHY_POWER_SUPPLY_PATH="$test_tmp" "$ROOT/bin/omarchy-battery-present" || fail "battery presence accepts a System-scoped battery beside a peripheral"
pass "battery presence accepts a System-scoped battery beside a peripheral"
rm -r "$test_tmp/CMB0"
if OMARCHY_POWER_SUPPLY_PATH="$test_tmp" "$ROOT/bin/omarchy-battery-present"; then
fail "battery presence ignores a peripheral's Device-scoped battery"
fi
pass "battery presence ignores a peripheral's Device-scoped battery"
+83 -7
View File
@@ -8,20 +8,40 @@ tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
mkdir -p "$tmp_dir/bin"
mkdir -p "$tmp_dir/power/BAT0"
printf '900000\n' >"$tmp_dir/power/BAT0/current_now"
printf '12000000\n' >"$tmp_dir/power/BAT0/voltage_now"
# Apple Silicon: a battery not called BAT*, power_now signed by direction, and
# the thresholds and cycle count only under the native path.
mkdir -p "$tmp_dir/power/macsmc-battery"
printf '%s\n' '-10800000' >"$tmp_dir/power/macsmc-battery/power_now"
printf '75\n' >"$tmp_dir/power/macsmc-battery/charge_control_start_threshold"
printf '80\n' >"$tmp_dir/power/macsmc-battery/charge_control_end_threshold"
printf '212\n' >"$tmp_dir/power/macsmc-battery/cycle_count"
cat >"$tmp_dir/bin/upower" <<'STUB'
#!/bin/bash
# A wireless mouse enumerates first, as a battery_ device that is not a power
# supply; the machine's own battery follows it.
if [[ $1 == "-e" ]]; then
echo "/org/freedesktop/UPower/devices/battery_BAT0"
echo "/org/freedesktop/UPower/devices/line_power_macsmc_ac"
echo "/org/freedesktop/UPower/devices/battery_hidpp_battery_0"
echo "/org/freedesktop/UPower/devices/battery_${OMARCHY_TEST_NATIVE_PATH//-/_}"
exit 0
fi
if [[ $1 == "-i" && $2 == */battery_hidpp_battery_0 ]]; then
cat <<'INFO'
native-path: hidpp_battery_0
model: Wireless Mouse
power supply: no
state: discharging
percentage: 5%
INFO
exit 0
fi
if [[ $1 == "-i" ]]; then
cat <<'INFO'
native-path: BAT0
native-path: macsmc-battery
power supply: yes
state: discharging
energy: 28.3 Wh
energy-full: 56.7 Wh
@@ -36,13 +56,69 @@ exit 1
STUB
chmod +x "$tmp_dir/bin/upower"
shell_output=$(OMARCHY_POWER_SUPPLY_PATH="$tmp_dir/power" PATH="$tmp_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
shell_output=$(OMARCHY_TEST_NATIVE_PATH=macsmc-battery OMARCHY_POWER_SUPPLY_PATH="$tmp_dir/power" PATH="$tmp_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
grep -Fx $'percentage\t51%' <<<"$shell_output" >/dev/null || fail "battery status reports percentage"
grep -Fx $'percentage\t51%' <<<"$shell_output" >/dev/null || fail "battery status reports the machine's battery, not the mouse's"
grep -Fx $'state\tdischarging' <<<"$shell_output" >/dev/null || fail "battery status reports state"
grep -Fx $'rate\t10.8W' <<<"$shell_output" >/dev/null || fail "battery status reports live sysfs power rate"
grep -Fx $'size\t56Wh' <<<"$shell_output" >/dev/null || fail "battery status reports full capacity"
grep -Fx $'time\t2h 30m' <<<"$shell_output" >/dev/null || fail "battery status reports remaining time"
grep -Fx $'cycles\t212' <<<"$shell_output" >/dev/null || fail "battery status reports native-path cycle count"
grep -Fx $'threshold\t75-80%' <<<"$shell_output" >/dev/null || fail "battery status reports native-path charge thresholds"
# The BAT* name the script used to key on is one of many; CMB0 is another.
mkdir -p "$tmp_dir/power/CMB0"
printf '7300000\n' >"$tmp_dir/power/CMB0/power_now"
sed -i 's/native-path: macsmc-battery/native-path: CMB0/' "$tmp_dir/bin/upower"
generic_output=$(OMARCHY_TEST_NATIVE_PATH=CMB0 OMARCHY_POWER_SUPPLY_PATH="$tmp_dir/power" PATH="$tmp_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
grep -Fx $'rate\t7.3W' <<<"$generic_output" >/dev/null || fail "battery status accepts arbitrary UPower battery paths"
pass "battery status supports Apple Silicon and arbitrary native battery paths"
# An 80% hold threshold doesn't trip while the battery is still below it.
hold_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir" "$hold_dir"' EXIT
mkdir -p "$hold_dir/bin" "$hold_dir/power/BAT0" "$hold_dir/power/ac"
printf 'Mains\n' >"$hold_dir/power/ac/type"
printf '1\n' >"$hold_dir/power/ac/online"
printf '80\n' >"$hold_dir/power/BAT0/charge_control_end_threshold"
cat >"$hold_dir/bin/upower" <<'STUB'
#!/bin/bash
if [[ $1 == "-e" ]]; then
echo "/org/freedesktop/UPower/devices/battery_BAT0"
exit 0
fi
if [[ $1 == "-i" ]]; then
cat <<'INFO'
native-path: BAT0
power supply: yes
state: charging
energy-full: 69.6 Wh
energy-rate: 0.1 W
time to full: 0.2 hours
percentage: 79.5%
charge-end-threshold: 80%
INFO
exit 0
fi
exit 1
STUB
chmod +x "$hold_dir/bin/upower"
hold_output=$(OMARCHY_POWER_SUPPLY_PATH="$hold_dir/power" PATH="$hold_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
grep -Fx $'state\tcharging' <<<"$hold_output" >/dev/null || fail "a hold below its threshold is still charging"
pass "battery status doesn't trip a hold early"
# Once the battery reaches the threshold, idle charging is holding.
sed -i 's/percentage: 79.5%/percentage: 80.0%/' "$hold_dir/bin/upower"
held_output=$(OMARCHY_POWER_SUPPLY_PATH="$hold_dir/power" PATH="$hold_dir/bin:$PATH" "$ROOT/bin/omarchy-battery-status" --shell)
grep -Fx $'percentage\t80%' <<<"$held_output" >/dev/null || fail "threshold percentage still displays as 80%"
grep -Fx $'state\tholding' <<<"$held_output" >/dev/null || fail "idle charging at the threshold is holding"
pass "battery status reports holding once the raw percentage reaches the threshold"
if matches=$(rg -n 'omarchy-battery-(capacity|remaining|remaining-time)' "$ROOT/bin" "$ROOT/test" "$ROOT/shell" "$ROOT/docs"); then
fail "battery status owns capacity and remaining calculations" "$matches"
+71
View File
@@ -0,0 +1,71 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command lua
HOME="$(mktemp -d)" OMARCHY_PATH="$ROOT" lua <<'LUA'
package.path = os.getenv("OMARCHY_PATH") .. "/?.lua;" .. package.path
local binds, ran = {}, {}
hl = setmetatable({
dsp = setmetatable({}, {
__index = function(_, name)
return function(arg) return { dsp = name, arg = arg } end
end,
}),
bind = function(keys, dispatcher, opts)
table.insert(binds, { keys = keys, dispatcher = dispatcher, opts = opts })
end,
exec_cmd = function(command) table.insert(ran, command) end,
dispatch = function(dispatcher) table.insert(ran, dispatcher.dsp .. " " .. dispatcher.arg) end,
}, {
__index = function()
return function() return {} end
end,
})
require("default.hypr.helpers")
local function ran_since(fn)
local before = #ran
fn()
return table.concat({ table.unpack(ran, before + 1) }, "\n")
end
local options = { locked = true }
o.bind_hold("SUPER + X", "Start", "start-cmd", "Stop", "stop-cmd", options)
local press, release = binds[1], binds[2]
assert(press.keys == "SUPER + X" and release.keys == "SUPER + X", "both halves bind the same keys")
assert(press.opts.description == "Start" and release.opts.description == "Stop", "each half keeps its description")
assert(not press.opts.release and not press.opts.transparent, "the press half is an ordinary bind")
assert(release.opts.release and release.opts.transparent and release.opts.ignore_mods,
"the release half survives keys typed and modifiers changed during the hold")
assert(release.opts.non_consuming, "the release half leaves the key to apps when its press did not run")
assert(press.opts.locked and release.opts.locked, "caller options apply to both halves")
assert(options.release == nil and options.description == nil, "caller options are not modified")
assert(o.bind_commands[press.dispatcher] == "start-cmd" and o.bind_commands[release.dispatcher] == "stop-cmd",
"the keybindings menu can run each half")
assert(ran_since(release.dispatcher) == "", "a release without its press does nothing")
assert(ran_since(press.dispatcher) == "start-cmd", "the press runs its command")
assert(ran_since(release.dispatcher) == "stop-cmd", "the release ends what the press started")
assert(ran_since(release.dispatcher) == "", "a release runs once per press")
o.bind_hold("SUPER + R", "Start", "start-cmd", "Stop", "stop-cmd",
{ repeating = true, long_press = true, click = true, drag = true, locked = true })
local repeat_release, repeat_press = table.remove(binds), table.remove(binds)
assert(repeat_press.opts.repeating, "the press half keeps its options")
for _, key in ipairs({ "repeating", "long_press", "click", "drag" }) do
assert(repeat_release.opts[key] == nil, "the release half drops " .. key)
end
assert(repeat_release.opts.locked, "the release half keeps other options")
o.bind_hold("SUPER + Z", "Start", hl.dsp.exec_cmd("start-dsp"), "Stop", function() table.insert(ran, "stop-fn") end)
assert(ran_since(binds[3].dispatcher) == "exec_cmd start-dsp", "dispatchers are dispatched")
assert(ran_since(binds[4].dispatcher) == "stop-fn", "functions are called")
assert(ran_since(binds[2].dispatcher) == "", "each hold binding tracks its own press")
LUA
pass "hold bindings pair each release with its press"
+89
View File
@@ -0,0 +1,89 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
omarchy_path="$test_tmp/omarchy"
opt_path="$test_tmp/opt"
cdm="$opt_path/WidevineCdm/chromium"
calls="$test_tmp/calls.log"
mkdir -p "$stub_bin" "$test_tmp/home" "$omarchy_path/install/helpers" "$omarchy_path/config"
# The real policy helper writes under /etc without sudo when run as root, so
# stand it in; this test covers only the CDM link. Its as_root goes through the
# sudo stub, which logs every link, whatever the test's EUID.
printf 'browser_policy_setup_dir() { :; }\nas_root() { sudo "$@"; }\n' >"$omarchy_path/install/helpers/browser-policy.sh"
cp "$ROOT/config/chromium-flags.conf" "$omarchy_path/config/"
for command in omarchy-pkg-add omarchy-pkg-aur-add omarchy-install-chromium-copy-url omarchy-install-chromium-ytdlp omarchy-theme-set-browser omarchy-lifecycle-dispatch; do
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/$command"
done
cat >"$stub_bin/sudo" <<'SH'
#!/bin/bash
printf 'sudo:%s\n' "$*" >>"$TEST_LOG"
"$@"
SH
chmod +x "$stub_bin"/*
reset_opt() {
rm -rf "$opt_path"
mkdir -p "$opt_path"
local dir
for dir in "$@"; do
mkdir -p "$opt_path/$dir"
done
}
run() {
: >"$calls"
HOME="$test_tmp/home" OMARCHY_PATH="$omarchy_path" OMARCHY_OPT_PATH="$opt_path" \
TEST_LOG="$calls" PATH="$stub_bin:$PATH" "$@" >/dev/null
}
install_browser() {
run bash "$ROOT/bin/omarchy-install-browser" "$1"
}
links_made() {
grep -c '^sudo:ln ' "$calls" || true
}
assert_cdm_linked() {
[[ -L $opt_path/$1/WidevineCdm && $(readlink "$opt_path/$1/WidevineCdm") == "$cdm" ]] ||
fail "$2" "$(ls -la "$opt_path/$1" 2>&1)"
}
for browser in brave brave-origin; do
install_dir=$browser-bin
reset_opt WidevineCdm/chromium "$install_dir"
install_browser "$browser"
assert_cdm_linked "$install_dir" "$browser install links the system Widevine CDM into $install_dir"
install_browser "$browser"
(( $(links_made) == 0 )) || fail "$browser reinstall keeps the existing CDM link" "$(<"$calls")"
assert_cdm_linked "$install_dir" "$browser reinstall keeps the existing CDM link"
pass "$browser install links the system Widevine CDM into $install_dir once"
reset_opt "$install_dir"
install_browser "$browser"
[[ ! -e $opt_path/$install_dir/WidevineCdm && ! -L $opt_path/$install_dir/WidevineCdm ]] ||
fail "$browser install links nothing without the widevine package"
pass "$browser install links nothing without the widevine package"
reset_opt WidevineCdm/chromium "$install_dir" own-cdm
ln -s "$opt_path/own-cdm" "$opt_path/$install_dir/WidevineCdm"
install_browser "$browser"
[[ $(readlink "$opt_path/$install_dir/WidevineCdm") == "$opt_path/own-cdm" ]] ||
fail "$browser install keeps a CDM the browser already has"
pass "$browser install keeps a CDM the browser already has"
done
reset_opt WidevineCdm/chromium
install_browser chromium
(( $(links_made) == 0 )) || fail "Chromium install leaves Widevine to the widevine package" "$(<"$calls")"
pass "Chromium install leaves Widevine to the widevine package"
+25 -4
View File
@@ -9,6 +9,11 @@ fix_t2="$ROOT/install/hardware/apple/fix-t2.sh"
all="$ROOT/install/hardware/all.sh"
migration="$ROOT/migrations/1786391100.sh"
# Apple Silicon's BCM4387 is in the ID list, but there the firmware supplicant
# is the one that works, so the leaf and the migration step aside: a fresh
# install marks the migration done only for its first user.
grep -Fq 'omarchy-hw-aarch64-apple && exit 0' "$migration" || fail "Broadcom migration excludes Apple Silicon"
grep -q 'apple/fix-brcmfmac-supplicant.sh' "$all" ||
fail "the brcmfmac quirk runs during hardware setup"
@@ -61,13 +66,19 @@ printf '\t%s' "$@" >>"$TEST_LOG"
printf '\n' >>"$TEST_LOG"
SH
cat >"$stub_bin/omarchy-hw-aarch64-apple" <<'SH'
#!/bin/bash
(( ${APPLE_SILICON:-0} == 1 ))
SH
chmod +x "$stub_bin"/*
# The leaf reads the vendor from an absolute path, so point it at a fixture by
# running with a fake root on PATH-independent state. pipefail is on, so a
# grep -q gate would go silent here the way #6608 did.
run_leaf() {
local vendor="$1" wifi_id="${2:-}" t2="${3:-0}"
local vendor="$1" wifi_id="${2:-}" t2="${3:-0}" apple_silicon="${4:-0}"
rm -rf "$test_tmp/etc"
mkdir -p "$test_tmp/etc"
printf '%s' "$vendor" >"$test_tmp/dmi/sys_vendor"
@@ -78,7 +89,7 @@ run_leaf() {
-e "s|/etc/modprobe.d|$test_tmp/etc/modprobe.d|g" \
"$leaf" >"$script"
WIFI_ID="$wifi_id" T2_HARDWARE="$t2" PATH="$stub_bin:$PATH" \
WIFI_ID="$wifi_id" T2_HARDWARE="$t2" APPLE_SILICON="$apple_silicon" PATH="$stub_bin:$PATH" \
bash -eE -o pipefail -c 'source "$1"' bash "$script" </dev/null
}
@@ -97,6 +108,10 @@ for wifi_id in 43ba 43bb 43bc 43a3 43dc 4464 4425 4433; do
done
pass "every brcmfmac part on a Mac without a T2 gets the quirk"
run_leaf "Apple Inc." 4433 0 1 >/dev/null
[[ ! -f $conf ]] || fail "Apple Silicon is excluded from the Intel Mac quirk"
pass "Apple Silicon is excluded from the Intel Mac quirk"
# Older Macs report the vendor differently.
run_leaf "Apple Computer, Inc." 43ba 0 >/dev/null
[[ -f $conf ]] || fail "the older Apple vendor string is recognized"
@@ -120,11 +135,11 @@ pass "a Mac with no wireless device is left alone"
# Installs that predate the quirk never ran the leaf, so the migration has to
# reach them. It runs as the user under pipefail, the context #6608 was about.
run_migration() {
local vendor="$1" wifi_id="${2:-}" t2="${3:-0}"
local vendor="$1" wifi_id="${2:-}" t2="${3:-0}" apple_silicon="${4:-0}"
printf '%s' "$vendor" >"$test_tmp/dmi/sys_vendor"
: >"$calls"
WIFI_ID="$wifi_id" T2_HARDWARE="$t2" PATH="$stub_bin:$PATH" TEST_LOG="$calls" \
WIFI_ID="$wifi_id" T2_HARDWARE="$t2" APPLE_SILICON="$apple_silicon" PATH="$stub_bin:$PATH" TEST_LOG="$calls" \
OMARCHY_BRCMFMAC_DMI_VENDOR="$test_tmp/dmi/sys_vendor" \
OMARCHY_BRCMFMAC_CONF="$conf" \
bash -euo pipefail "$migration" >/dev/null
@@ -148,6 +163,12 @@ run_migration "Apple Inc." 4488 1
[[ ! -s $calls ]] || fail "a repaired install is left untouched" "$(cat "$calls")"
pass "the migration is idempotent"
rm -rf "$test_tmp/etc"
run_migration "Apple Inc." 4433 0 1
[[ ! -e $conf ]] || fail "the migration leaves Apple Silicon Wi-Fi alone"
[[ ! -s $calls ]] || fail "the migration escalates nothing on Apple Silicon" "$(cat "$calls")"
pass "the migration leaves Apple Silicon Wi-Fi alone"
# The machine this was written for, with no T2 to fall back on.
rm -rf "$test_tmp/etc"
run_migration "Apple Inc." 43ba 0
+52 -2
View File
@@ -53,9 +53,19 @@ SH
chmod +x "$mock_bin"/*
# A copy of the command reads its platform file from a fixture root (none
# unless a case writes one, whatever the machine running the suite has
# installed) and its DRM connectors from a fixture directory.
platform_root="$test_tmp/platform"
mkdir -p "$platform_root" "$test_tmp/copy"
brightness_display="$test_tmp/copy/omarchy-brightness-display"
platform_root_copy "$ROOT/bin/omarchy-brightness-display" "$brightness_display" "$platform_root"
sed -i "s|/sys/class/drm|$test_tmp/drm|g" "$brightness_display"
grep -qF "$test_tmp/drm/card" "$brightness_display" || fail "omarchy-brightness-display reads DRM connectors from the fixture"
run_brightness() {
CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" PATH="$mock_bin:$ROOT/bin:$PATH" \
"$ROOT/bin/omarchy-brightness-display" "$@"
CALL_LOG="$call_log" XDG_RUNTIME_DIR="$runtime_dir" \
PATH="$mock_bin:$ROOT/bin:$PATH" "$brightness_display" "$@"
}
brightness=$(run_brightness --monitor DP-1)
@@ -144,3 +154,43 @@ if PATH="$mock_bin:$PATH" "$ROOT/bin/omarchy-hyprland-monitor-focused-apple"; th
fail "focused non-Apple display is not detected as Apple"
fi
pass "named Apple display is detected independently of focus"
# A platform whose display driver has no DDC channel says so: an external
# monitor whose connector has no ddc node is not probed and never dims the
# built-in panel in its place.
mkdir -p "$test_tmp/drm/card2-USB-1" "$test_tmp/drm/card2-eDP-1"
# The last line needs no newline.
printf '%s\n%s' '# no DDC channel on the display driver' 'ddc-require-connector-ddc' >"$platform_root/displays.conf"
: >"$call_log"
if run_brightness --monitor USB-1 >/dev/null 2>&1; then
fail "an external monitor without a ddc node has no brightness backend"
fi
if run_brightness --no-osd --monitor USB-1 +5% >/dev/null 2>&1; then
fail "setting an external monitor without a ddc node fails"
fi
printf 'ddc-require-connector-ddc\r\n' >"$platform_root/displays.conf"
if run_brightness --monitor USB-1 >/dev/null 2>&1; then
fail "a displays.conf saved with CRLF line ends reads the same"
fi
printf '%s\n%s' '# no DDC channel on the display driver' 'ddc-require-connector-ddc' >"$platform_root/displays.conf"
[[ ! -s $call_log ]] || fail "an external monitor without a ddc node probes nothing" "$(<"$call_log")"
brightness=$(run_brightness --monitor eDP-1)
(( brightness == 40 )) || fail "the built-in panel still uses the kernel backlight" "actual: $brightness"
pass "a platform without DDC channels skips DDC for a connector without one"
rm -f "$runtime_dir/omarchy-brightness-display-ddc/"*
: >"$test_tmp/drm/card2-USB-1/ddc"
brightness=$(DDC_CONNECTOR=USB-1 run_brightness --monitor USB-1)
(( brightness == 50 )) || fail "an external monitor with a ddc node uses DDC" "actual: $brightness"
pass "a platform without DDC channels still uses DDC where a connector has one"
# Without the directive, every external monitor is probed, ddc node or not.
rm -f "$runtime_dir/omarchy-brightness-display-ddc/"* "$test_tmp/drm/card2-USB-1/ddc"
for conf in "" "ddc-require-connector-ddc extra" "ddc-future"; do
printf '%s\n' "$conf" >"$platform_root/displays.conf"
rm -f "$runtime_dir/omarchy-brightness-display-ddc/"*
brightness=$(DDC_CONNECTOR=USB-1 run_brightness --monitor USB-1)
(( brightness == 50 )) || fail "an external monitor is probed without the directive" "conf: $conf, actual: $brightness"
done
rm -f "$platform_root/displays.conf"
pass "every external monitor is probed without the directive"
@@ -69,6 +69,51 @@ for channel in stable rc edge dev; do
pass "$channel starts cold, authorizes the switch per command, runs the refresh hook cold, hands off to one update authorization and exits cold"
done
# Every aarch64 platform switches to edge and dev, copying its own templates.
for platform in aarch64 aarch64-apple; do
case $platform in
aarch64-apple) templates=default/pacman/aarch64-apple ;;
*) templates=default/pacman/aarch64 ;;
esac
for channel in edge dev; do
# dev refreshes from the checkout it links, on edge.
pacman_channel=$channel root=$SUDO_TEST_ROOT
[[ $channel != "dev" ]] || pacman_channel=edge root=$SUDO_TEST_HOME/omarchy
reset_boundary
SUDO_TEST_PLATFORM=$platform run_channel "$channel" || fail "$platform $channel failed" "$(<"$boundary_tmp/output")"
assert_scoped_channel "$platform $channel"
grep -Fqx "step:cp -f $root/$templates/pacman-$pacman_channel.conf /etc/pacman.conf" "$SUDO_TEST_LOG" ||
fail "$platform $channel copies its own template" "$(<"$SUDO_TEST_LOG")"
done
done
pass "aarch64 platforms switch to edge and dev through their own templates"
# stable and rc would install the release line, which has no aarch64 support:
# an aarch64 machine refuses them before anything changes.
for platform in aarch64 aarch64-apple; do
for channel in stable rc; do
reset_boundary
if SUDO_TEST_PLATFORM=$platform run_channel "$channel"; then fail "$platform accepted $channel"; fi
if grep -Eq '^step:|^sudo -N ' "$SUDO_TEST_LOG"; then fail "$platform $channel was refused before any change" "$(<"$SUDO_TEST_LOG")"; fi
grep -q "Omarchy has no $channel channel for $platform" "$boundary_tmp/output" || fail "$platform $channel: the refusal says why" "$(<"$boundary_tmp/output")"
assert_boundary_cold "$platform $channel"
done
done
pass "aarch64 platforms refuse stable and rc before any change"
# A channel the platform has no template for stops before anything, the dev
# confirmation included.
mv "$SUDO_TEST_ROOT/default/pacman/aarch64-apple/pacman-edge.conf" "$boundary_tmp/saved-template"
for channel in edge dev; do
reset_boundary
if SUDO_TEST_PLATFORM=aarch64-apple run_channel "$channel"; then fail "a channel without a template was accepted ($channel)"; fi
if grep -Eq '^step:|^sudo -N ' "$SUDO_TEST_LOG"; then fail "$channel was refused before any change" "$(<"$SUDO_TEST_LOG")"; fi
grep -q "Omarchy has no edge channel for aarch64-apple" "$boundary_tmp/output" || fail "the refusal says why" "$(<"$boundary_tmp/output")"
assert_boundary_cold "missing template $channel"
done
mv "$boundary_tmp/saved-template" "$SUDO_TEST_ROOT/default/pacman/aarch64-apple/pacman-edge.conf"
pass "a channel without a template for the platform is refused before any change"
reset_boundary
wrapper="$SUDO_TEST_HOME/omarchy/default/omarchy/sudo-no-update/sudo"
mv "$wrapper" "$boundary_tmp/saved-wrapper"
@@ -81,6 +126,40 @@ assert_boundary_cold "stale checkout"
mv "$boundary_tmp/saved-wrapper" "$wrapper"
pass "a stale dev checkout is rejected before linking or privileged work"
# On aarch64 the checkout's own refresh keeps the machine's repositories only
# once it tells platforms apart: a checkout without that is refused the same way.
checkout="$SUDO_TEST_HOME/omarchy"
for required in bin/omarchy-hw-platform install/helpers/pacman.sh; do
for platform in aarch64; do
reset_boundary
mv "$checkout/$required" "$boundary_tmp/saved-required"
if SUDO_TEST_PLATFORM=$platform run_channel dev; then fail "$platform: a dev checkout without $required was accepted"; fi
mv "$boundary_tmp/saved-required" "$checkout/$required"
if grep -Eq '^step:omarchy-(dev-link|state)|^step:pacman|^sudo -N ' "$SUDO_TEST_LOG"; then
fail "$platform: a dev checkout without $required changed the system before rejection" "$(<"$SUDO_TEST_LOG")"
fi
grep -q "Update the checkout before switching to dev; on $platform it needs $required" "$boundary_tmp/output" ||
fail "$platform: the rejection names $required" "$(<"$boundary_tmp/output")"
assert_boundary_cold "$platform checkout without $required"
done
done
pass "on aarch64 a dev checkout that can't keep the machine's repositories is rejected before linking"
# The checkout's own refresh copies its own templates, so one without them for
# this platform is refused before linking.
template=$checkout/default/pacman/aarch64/mirrorlist-edge
mv "$template" "$boundary_tmp/saved-template"
reset_boundary
if SUDO_TEST_PLATFORM=aarch64 run_channel dev; then fail "a dev checkout without its templates was accepted"; fi
mv "$boundary_tmp/saved-template" "$template"
if grep -Eq '^step:omarchy-(dev-link|state)|^step:pacman|^sudo -N ' "$SUDO_TEST_LOG"; then
fail "a dev checkout without its templates changed the system before rejection" "$(<"$SUDO_TEST_LOG")"
fi
grep -q "Update the checkout before switching to dev; it has no edge templates for aarch64" "$boundary_tmp/output" ||
fail "the rejection names the missing templates" "$(<"$boundary_tmp/output")"
assert_boundary_cold "checkout without templates"
pass "a dev checkout without its templates for the platform is rejected before linking"
reset_boundary
OMARCHY_PATH="$SUDO_TEST_HOME/omarchy" run_channel stable || fail "leaving dev failed" "$(<"$boundary_tmp/output")"
assert_scoped_channel "dev to stable"
+27
View File
@@ -108,6 +108,7 @@ pass "default bar widget ids resolve to manifests and entry points"
ROOT="$ROOT" python3 <<'PY'
import os
import re
import sys
from pathlib import Path
@@ -173,6 +174,32 @@ for source, destination, legacy in package_defaults:
if destination and (source not in pkgbuild or destination not in pkgbuild):
errors.append(f"PKGBUILD does not explicitly install {source} -> {destination}")
# A user unit has to be on the machine before anything turns it on: shipped in
# /usr/lib/systemd/user, or copied out of default/ by the command enabling it.
def installs_first(text):
source = re.search(r'(\w+)="[^"\n]*default/systemd/user/', text)
held = rf"|\$\{{?{source[1]}\b" if source else ""
copy = re.search(rf"^[ \t]*(?:sudo[ \t]+)?(?:install|cp)\b[^\n]*(?:default/systemd/user/{held})", text, re.MULTILINE)
enable = text.find("systemctl --user enable")
return bool(copy) and (enable < 0 or copy.start() < enable)
named = 'unit="$OMARCHY_PATH/default/systemd/user/a.service"\n'
if installs_first(named + "systemctl --user enable a.service\n"):
errors.append("unit check accepts a command that names a unit's source but never installs it")
if installs_first(named + 'systemctl --user enable a.service\ninstall -Dm644 "$unit" "$target"\n'):
errors.append("unit check accepts a command that installs a unit after enabling it")
scripts = [path.read_text(errors="ignore") for folder in ("bin", "install", "migrations")
for path in (root / folder).rglob("*") if path.is_file()]
for unit in sorted(path.name for path in (root / "default/systemd/user").glob("*.service")):
if f"/usr/lib/systemd/user/{unit}" in pkgbuild:
continue
named = [text for text in scripts if unit in text]
if not any(installs_first(text) for text in named):
errors.append(f"PKGBUILD does not ship default/systemd/user/{unit} and no command installs it")
elif any("systemctl --user enable" in text and not installs_first(text) for text in named):
errors.append(f"{unit} is enabled without being shipped by PKGBUILD or installed first")
# Existing users have an absolute wants symlink to the old unit path, and the
# migration that repoints it only runs for users who run an update -- the
# opposite of who the notifier is for. Dropping this alias strands them.
@@ -0,0 +1,91 @@
#!/bin/bash
#
# The migration that puts systemd.tty.term.console=dumb into effect on an
# existing install: the package has already written the parameter into
# omarchy-defaults.conf, so what is left is rebuilding the boot image, once per
# machine, and only where it is needed.
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
migration="$ROOT/migrations/1791581004.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
stub_bin="$test_dir/bin"
mkdir -p "$stub_bin"
# sudo just drops the prefix; limine-mkinitcpio records that it ran, and fails
# when told to.
printf '#!/bin/bash\nexec "$@"\n' >"$stub_bin/sudo"
# A rebuild writes the new command line into the boot menu, unless it is the
# kind that builds nothing and still exits 0.
cat >"$stub_bin/limine-mkinitcpio" <<'STUB'
#!/bin/bash
echo rebuilt >>"${REBUILDS:?}"
[[ -z ${REBUILD_FAILS:-} ]] || exit 1
[[ -n ${REBUILD_BUILDS_NOTHING:-} ]] || printf ' cmdline: %s\n' "${NEW_CMDLINE:?}" >"${LIMINE_CONF:?}"
STUB
chmod +x "$stub_bin"/*
old_cmdline="root=PARTUUID=1 rw quiet splash initramfs_async=0"
new_cmdline="$old_cmdline systemd.tty.term.console=dumb"
run() { # booted cmdline, defaults conf
: >"$test_dir/rebuilds"
printf '%s\n' "$1" >"$test_dir/cmdline"
printf ' cmdline: %s\n' "$1" >"$test_dir/limine.conf"
PATH="$stub_bin:$ROOT/bin:$PATH" REBUILDS="$test_dir/rebuilds" \
LIMINE_CONF="$test_dir/limine.conf" NEW_CMDLINE="$new_cmdline" OMARCHY_LIMINE_CONF="$test_dir/limine.conf" \
OMARCHY_RUNNING_CMDLINE="$test_dir/cmdline" OMARCHY_LIMINE_DEFAULTS_CONF="$2" \
OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" bash -euo pipefail "$migration" >/dev/null
}
rebuilds() { wc -l <"$test_dir/rebuilds"; }
# omarchy-migrate runs a migration with bash -euo pipefail, and so does run:
# that is what stops a failed rebuild before the marker is written.
if REBUILD_FAILS=1 run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"; then
fail "a rebuild that failed was reported as a success"
fi
[[ $(rebuilds) == 1 && ! -e $test_dir/marker ]] || fail "a failed rebuild was marked as done"
pass "a rebuild that fails stops the migration and leaves no marker"
# limine-mkinitcpio exits 0 past a kernel it could not build.
if REBUILD_BUILDS_NOTHING=1 run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"; then
fail "a rebuild that changed nothing was reported as a success"
fi
[[ ! -e $test_dir/marker ]] || fail "a rebuild that changed nothing was marked as done"
pass "a rebuild that exits 0 without changing the boot menu leaves the migration pending"
run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"
[[ $(rebuilds) == 1 && -e $test_dir/marker ]] ||
fail "a machine booted without the parameter did not get its boot image rebuilt"
pass "the next run rebuilds the boot image, with the packaged defaults, and marks it"
run "$old_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"
[[ $(rebuilds) == 0 ]] || fail "the rebuild ran a second time before a reboot"
pass "the rebuild runs once per machine, not once per user"
rm -f "$test_dir/marker"
run "$new_cmdline" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf"
[[ $(rebuilds) == 0 && ! -e $test_dir/marker ]] || fail "a machine that already boots with the parameter was rebuilt"
pass "a machine that already boots with the parameter is left alone"
grep -v "tty.term.console" "$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" >"$test_dir/edited.conf"
run "$old_cmdline" "$test_dir/edited.conf"
[[ $(rebuilds) == 0 ]] || fail "a machine whose defaults leave the parameter out was rebuilt anyway"
pass "a machine whose defaults were edited to leave the parameter out is left alone"
# No Limine: the stand-in goes, and the PATH holds only what the migration
# needs, so a limine-mkinitcpio installed on the machine running this test is
# not found either.
rm "$stub_bin/limine-mkinitcpio"
mkdir -p "$test_dir/tools"
for tool in bash grep install cat sed tail; do ln -s "$(command -v "$tool")" "$test_dir/tools/$tool"; done
: >"$test_dir/rebuilds"
printf '%s\n' "$old_cmdline" >"$test_dir/cmdline"
PATH="$stub_bin:$ROOT/bin:$test_dir/tools" REBUILDS="$test_dir/rebuilds" \
OMARCHY_RUNNING_CMDLINE="$test_dir/cmdline" OMARCHY_LIMINE_DEFAULTS_CONF="$ROOT/etc/limine-entry-tool.d/omarchy-defaults.conf" \
OMARCHY_LIMINE_REBUILD_MARKER="$test_dir/marker" "$test_dir/tools/bash" -euo pipefail "$migration" >/dev/null
[[ ! -e $test_dir/marker ]] || fail "a machine without Limine was marked as rebuilt"
pass "a machine without Limine is left alone"
+3
View File
@@ -125,6 +125,9 @@ for setup_command in \
ln -s omarchy-test-setup-call "$mock_bin/$setup_command"
done
# The real browser installer below calls the platform's post-install hook; on a
# Mac with the platform package installed that would run, so answer it here.
printf '#!/bin/bash\nexit 0\n' >"$mock_bin/omarchy-lifecycle-dispatch"
chmod +x "$mock_bin"/*
export HOME="$test_home"
+1 -1
View File
@@ -22,7 +22,7 @@ cat >"$mock_bin/omarchy-font-set" <<'SH'
printf 'font:%s\n' "$*" >>"$OMARCHY_TEST_LOG"
SH
for command in omarchy-pkg-aur-add omarchy-install-emacs omazed omarchy-theme-set-vscode omarchy-install-gaming-gpu-lib32; do
for command in omarchy-pkg-aur-add omarchy-install-emacs omazed omarchy-theme-set-vscode omarchy-install-gaming-gpu-lib32 omarchy-lifecycle-dispatch; do
cat >"$mock_bin/$command" <<'SH'
#!/bin/bash
exit 0
+47 -7
View File
@@ -212,21 +212,41 @@ pass "selection updates are atomic for concurrent readers"
lua <<'LUA'
local root = os.getenv("ROOT")
local configured, bindings = false, {}
local selected, installed, bindings = nil, true, {}
io.popen = function(command)
assert(command == "omarchy-default-dictation 2>/dev/null")
return { read = function() return selected end, close = function() end }
end
o = {
shell_succeeds = function(command)
assert(command == "omarchy-default-dictation")
return configured
cmd_missing = function(command)
assert(command == selected)
return not installed
end,
bind = function(...) table.insert(bindings, {...}) end,
bind_hold = function(keys, _, press, _, release, options)
table.insert(bindings, { keys, press, options })
table.insert(bindings, { keys, release, options })
end,
}
dofile(root .. "/default/hypr/bindings/dictation.lua")
assert(#bindings == 0, "unconfigured dictation leaves application keys available")
configured = true
selected = "some-backend"
dofile(root .. "/default/hypr/bindings/dictation.lua")
assert(#bindings == 5, "any configured backend receives the same bindings")
assert(#bindings == 5, "any installed backend receives the same bindings")
local function binds(keys, command)
for _, binding in ipairs(bindings) do
if binding[1] == keys and binding[2] == command then return true end
end
end
for _, keys in ipairs({ "F9", "ALT + Alt_R" }) do
assert(binds(keys, "omarchy-dictation start") and binds(keys, "omarchy-dictation stop"),
keys .. " starts dictation on press and stops it on release")
end
bindings, installed = {}, false
dofile(root .. "/default/hypr/bindings/dictation.lua")
assert(#bindings == 0, "a selected backend that is not installed leaves application keys available")
LUA
pass "dictation shortcuts require a selection without limiting backend names"
pass "dictation shortcuts require an installed selection without limiting backend names"
# Fresh users receive the backend choice through the settings package's skel.
cp "$ROOT/config/omarchy/defaults/dictation" "$config"
@@ -234,6 +254,26 @@ cp "$ROOT/config/omarchy/defaults/dictation" "$config"
printf '%s\n' voxtype > "$config"
pass "fresh users get Superwhisper as their default backend"
# On aarch64, where Superwhisper has no build, user setup drops that preset; an
# owner's own selection, and every x86_64 preset, stay.
leaf_bin="$test_tmp/leaf-bin"
mkdir -p "$leaf_bin"
printf '#!/bin/bash\n[[ ${TEST_ARCH:-aarch64} == x86_64 ]]\n' >"$leaf_bin/omarchy-hw-x86"
printf '#!/bin/bash\n[[ -z ${TEST_HAS_SUPERWHISPER:-} ]]\n' >"$leaf_bin/omarchy-cmd-missing"
chmod +x "$leaf_bin"/*
run_leaf() { env "$@" PATH="$leaf_bin:$PATH" bash -c 'source "$1"' bash "$ROOT/install/user/dictation-default.sh"; }
cp "$ROOT/config/omarchy/defaults/dictation" "$config"
run_leaf
[[ ! -e $config ]] || fail "aarch64 user setup drops the Superwhisper preset"
printf '%s\n' voxtype >"$config"
run_leaf
[[ $(<"$config") == "voxtype" ]] || fail "aarch64 user setup keeps a selection someone made"
cp "$ROOT/config/omarchy/defaults/dictation" "$config"
run_leaf TEST_ARCH=x86_64
[[ $(<"$config") == "superwhisper" ]] || fail "x86_64 keeps the Superwhisper preset"
printf '%s\n' voxtype >"$config"
pass "aarch64 new users start with no dictation backend instead of an unbuildable one"
lua <<'LUA'
local root = os.getenv("ROOT")
package.path = root .. "/?.lua;" .. package.path
+819 -37
View File
@@ -4,46 +4,828 @@ set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir=$(mktemp -d)
trap 'rm -r "$tmp_dir"' EXIT
# omarchy-drive-password against fake drives. findmnt and lsblk describe a
# system drive under / and a data drive, and blkid has the empty cache a user
# sees until root runs blkid in that boot; cryptsetup is either a slot-table fake
# or the real binary on file-backed volumes; a chpasswd fake shows the login and
# root passwords never change. Each cryptsetup call is a crash point, so a run
# can be killed after every step and rerun, like a power loss. The runs repeat on an Apple fixture,
# where a fake boot package records the owner's slot through the real
# omarchy-lifecycle-dispatch; on the x86 fixture that is a no-op.
cat >"$tmp_dir/blkid" <<'EOF'
#!/bin/bash
echo /dev/test-luks
EOF
cat >"$tmp_dir/gum" <<'EOF'
#!/bin/bash
head -n 1 "$TEST_INPUTS"
sed -i '1d' "$TEST_INPUTS"
EOF
cat >"$tmp_dir/sudo" <<'EOF'
#!/bin/bash
printf '%s\n' "$@" >"$TEST_ARGS"
cat >"$TEST_STDIN"
EOF
chmod +x "$tmp_dir/blkid" "$tmp_dir/gum" "$tmp_dir/sudo"
export PATH="$tmp_dir:$ROOT/bin:$PATH"
export TEST_ARGS="$tmp_dir/args" TEST_INPUTS="$tmp_dir/inputs" TEST_STDIN="$tmp_dir/stdin"
printf '\n' >"$TEST_INPUTS"
if "$ROOT/bin/omarchy-drive-password" >/dev/null; then
fail "drive password rejects an empty passphrase"
# Root's dispatcher ignores the fixtures and sees this machine.
if (( EUID == 0 )) && [[ $("$ROOT/bin/omarchy-hw-platform") == "aarch64-apple" ]]; then
skip "running as root on Apple Silicon, where dispatch ignores fixtures; skipping"
exit 0
fi
[[ ! -e $TEST_ARGS ]] || fail "drive password does not run cryptsetup for an empty passphrase"
printf 'secret123\n*\n' >"$TEST_INPUTS"
if "$ROOT/bin/omarchy-drive-password" >/dev/null; then
fail "drive password rejects a mismatched confirmation"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
old_password=old-password
new_password='new pass:word'
recovery_key=recovery-passphrase
data_password=data-password
journal=$tmp/state/omarchy/drive-password.state
mkdir -p "$tmp/bin" "$tmp/real" "$tmp/dev"
cat >"$tmp/crash.sh" <<'SH'
crash_point() {
local count
count=$(( $(cat "$TEST_TMP/steps") + 1 ))
echo "$count" >"$TEST_TMP/steps"
printf '%s %s\n' "$count" "$1" >>"$TEST_TMP/trace"
if (( count == ${CRASH_AT:-0} )); then
kill -9 "$TEST_PID"
# An orphan outlives the script for a moment, like a cryptsetup child.
if [[ -n ${CRASH_ORPHAN:-} ]]; then sleep 1; else kill -9 $$; fi
fi
}
SH
cat >"$tmp/bin/blkid" <<'SH'
#!/bin/bash
exit 2
SH
cat >"$tmp/bin/findmnt" <<'SH'
#!/bin/bash
[[ $* == "-no SOURCE /" && -z ${TEST_FINDMNT_FAIL:-} ]] || exit 1
echo '/dev/mapper/root[/@]'
SH
cat >"$tmp/bin/lsblk" <<'SH'
#!/bin/bash
if [[ $1 == "-dno" && $2 == "UUID" ]]; then
cat "$3.uuid"
exit
fi
[[ ! -e $TEST_ARGS ]] || fail "drive password does not run cryptsetup for a mismatched confirmation"
if [[ $* == "-nrpo NAME,FSTYPE" ]]; then
echo "/dev/fake-disk "
while IFS= read -r drive; do
printf '%s crypto_LUKS\n/dev/mapper/%s btrfs\n' "$drive" "${drive##*/}"
done <"$TEST_TMP/drives"
exit
fi
[[ $* == "-nsrpo NAME,TYPE,FSTYPE /dev/mapper/root" ]] || exit 98
cat "$TEST_TMP/root-ancestry"
SH
printf 'new password\nnew password\n' >"$TEST_INPUTS"
"$ROOT/bin/omarchy-drive-password" >/dev/null
cat >"$tmp/bin/omarchy-drive-select" <<'SH'
#!/bin/bash
printf '%s\n' "$@" >"$TEST_TMP/offered"
cat "$TEST_TMP/select"
SH
[[ $(<"$TEST_STDIN") == "new password" ]] || fail "drive password passes the validated passphrase without a newline"
grep -F 'cryptsetup luksChangeKey' "$TEST_ARGS" >/dev/null || fail "drive password changes the LUKS key"
grep -Fx /dev/test-luks "$TEST_ARGS" >/dev/null || fail "drive password targets the selected drive"
pass "drive password rejects empty and mismatched passphrases and passes validated input to cryptsetup"
cat >"$tmp/bin/gum" <<'SH'
#!/bin/bash
[[ $1 == "input" ]] || exit 97
printf '%s\n' "$*" >>"$TEST_TMP/prompts"
[[ -s $TEST_TMP/inputs ]] || exit 130
# Another key enrolled while the owner types the new password.
if [[ -n ${TEST_ENROLL_AT_PROMPT:-} && $* == *"$TEST_ENROLL_AT_PROMPT"* ]]; then
printf '3\tlater-key\n' >>"$TEST_ENROLL_DEVICE.slots"
fi
head -n 1 "$TEST_TMP/inputs"
sed -i '1d' "$TEST_TMP/inputs"
SH
# Runs as the caller, closing every descriptor above stderr as sudo does.
cat >"$tmp/bin/sudo" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_TMP/sudo-calls"
for fd in /proc/$$/fd/*; do
fd=${fd##*/}
(( fd > 2 && fd != 255 )) && eval "exec $fd>&-" 2>/dev/null
done
exec "$@"
SH
cat >"$tmp/bin/chpasswd" <<'SH'
#!/bin/bash
source "$TEST_TMP/crash.sh"
IFS= read -r line
name=${line%%:*}
{
grep -v "^$name " "$TEST_TMP/accounts" || true
printf '%s\t%s\n' "$name" "${line#*:}"
} >"$TEST_TMP/accounts.next"
mv -f "$TEST_TMP/accounts.next" "$TEST_TMP/accounts"
SH
# A LUKS1-style volume as a table of "slot<TAB>passphrase" lines next to the
# device: luksChangeKey adds the new key to a free slot, then removes the old,
# and luksKillSlot needs a key that opens another slot, as cryptsetup does.
cat >"$tmp/bin/cryptsetup" <<'SH'
#!/bin/bash
source "$TEST_TMP/crash.sh"
slot_of() {
local slot key
while IFS=$'\t' read -r slot key; do
[[ $key == "$1" && $slot != "${2:-}" ]] && { echo "$slot"; return; }
done <"$slots"
}
op=$1
shift
key_file="" token_type="" key_slot="" positional=()
while (( $# )); do
case $1 in
--key-file) key_file=$2; shift 2 ;;
--token-type) token_type=$2; shift 2 ;;
--key-slot) key_slot=$2; shift 2 ;;
--pbkdf | --iter-time) shift 2 ;;
-*) shift ;;
*) positional+=("$1"); shift ;;
esac
done
slots=${positional[0]}.slots
[[ -f $slots ]] || { echo "Device ${positional[0]} does not exist or access denied."; exit 4; }
if [[ $key_file == "-" ]]; then key=$(cat); elif [[ -n $key_file ]]; then key=$(cat "$key_file"); fi
[[ $op != "isLuks" ]] || exit 1
if [[ $op == "luksDump" ]]; then
echo "Keyslots:"
cut -f1 "$slots" | sed 's/^/ /; s/$/: luks2/'
[[ -z ${TEST_TOKEN_SLOT:-} ]] || printf 'Tokens:\n 0: luks2-keyring\n\tKeyslot: %s\n' "$TEST_TOKEN_SLOT"
printf 'Digests:\n 0: pbkdf2\n'
exit 0
fi
crash_point "cryptsetup $op"
[[ $op != "open" || -z ${TEST_OPEN_FAIL:-} ]] || exit 1
# An enrolled token unlocks its slot whatever key is given, unless the allowed
# token types exclude it, as with cryptsetup.
if [[ $op == "open" && -z $token_type && -n ${TEST_TOKEN_SLOT:-} ]]; then
echo "Key slot $TEST_TOKEN_SLOT unlocked."
exit 0
fi
target=""
[[ $op == "luksKillSlot" ]] && target=${positional[1]}
if [[ $op == "open" && -n $key_slot ]]; then
grep -qxF "$key_slot"$'\t'"$key" "$slots" || { echo "No key available with this passphrase."; exit 2; }
echo "Key slot $key_slot unlocked."
exit 0
fi
slot=$(slot_of "$key" "$target")
[[ -n $slot ]] || { echo "No key available with this passphrase."; exit 2; }
case $op in
open) echo "Key slot $slot unlocked." ;;
luksChangeKey)
[[ -z ${TEST_CHANGE_FAIL:-} ]] || exit 1
for (( next = 0; next < 32; next++ )); do
cut -f1 "$slots" | grep -qx "$next" || break
done
printf '%s\t%s\n' "$next" "$(cat "${positional[1]}")" >>"$slots"
crash_point "new key added"
[[ -z ${TEST_CHANGE_PARTIAL:-} ]] || exit 137
awk -F'\t' -v s="$slot" '$1 != s' "$slots" >"$slots.next"
mv -f "$slots.next" "$slots"
crash_point "old key removed"
;;
luksKillSlot)
[[ -z ${TEST_KILL_FAIL:-} ]] || exit 1
awk -F'\t' -v s="$target" '$1 != s' "$slots" >"$slots.next"
mv -f "$slots.next" "$slots"
crash_point "slot $target killed"
;;
*) exit 1 ;;
esac
SH
# The real cryptsetup, keeping the command's KDF but at a fast cost, counting
# the same crash points.
cat >"$tmp/real/cryptsetup" <<'SH'
#!/bin/bash
source "$TEST_TMP/crash.sh"
args=()
while (( $# )); do
case $1 in
--iter-time) shift 2 ;;
*) args+=("$1"); shift ;;
esac
done
if [[ ${args[0]} == "luksChangeKey" ]]; then
if [[ " ${args[*]} " == *" argon2id "* ]]; then
args+=(--pbkdf-force-iterations 4 --pbkdf-memory 32768)
else
args+=(--pbkdf-force-iterations 1000)
fi
fi
[[ ${args[0]} == "luksDump" || ${args[0]} == "isLuks" ]] || crash_point "cryptsetup ${args[0]}"
[[ ${args[0]} != "open" || -z ${TEST_OPEN_FAIL:-} ]] || exit 1
status=0
"$REAL_CRYPTSETUP" "${args[@]}" || status=$?
if (( status == 0 )) && [[ ${args[0]} == "luksChangeKey" || ${args[0]} == "luksKillSlot" ]]; then
crash_point "${args[0]} done"
fi
exit "$status"
SH
chmod +x "$tmp"/bin/* "$tmp/real/cryptsetup"
# The boot package's luks-slots, run by dispatch with an empty environment: it
# records its arguments and is a crash point like the rest.
fake_platform "$tmp/x86" x86
fake_platform "$tmp/apple" aarch64-apple
mkdir -p "$tmp/lifecycle/usr/lib/omarchy/mac-boot"
cat >"$tmp/lifecycle/usr/lib/omarchy/mac-boot/luks-slots" <<SH
#!/bin/bash
# --owner names the recorded owner slot, read-only; setup recorded slot 0. A Mac
# set up before slots were recorded has none (4); a record the header doesn't
# hold is a failure (1).
if [[ \$1 == --owner ]]; then
[[ ! -e $tmp/owner-query-fail ]] || { echo "The LUKS header has no key in the recorded owner slot 0." >&2; exit 1; }
[[ ! -e $tmp/owner-query-garbage ]] || { echo "slot zero"; exit 0; }
[[ ! -e $tmp/owner-slot ]] || { cat "$tmp/owner-slot"; exit 0; }
[[ ! -e $tmp/owner-unrecorded ]] || { echo "No owner key slot is recorded." >&2; exit 4; }
echo 0
exit 0
fi
[[ ! -e $tmp/record-fail ]] || exit 1
printf '%s\n' "\$*" >>"$tmp/slot-record"
[[ \$1 != owner=* ]] || echo "\${1#owner=}" >"$tmp/owner-slot"
count=\$(( \$(cat "$tmp/steps") + 1 ))
echo "\$count" >"$tmp/steps"
printf '%s slots recorded\n' "\$count" >>"$tmp/trace"
if (( count == \$(cat "$tmp/crash-at") )); then
kill -9 "\$PPID"
kill -9 \$\$
fi
SH
chmod 755 "$tmp/lifecycle/usr/lib/omarchy/mac-boot/luks-slots"
chmod -R go-w "$tmp/lifecycle"
export TEST_TMP=$tmp OMARCHY_PATH=$ROOT XDG_STATE_HOME=$tmp/state SUDO_USER=owner OMARCHY_LIFECYCLE_ROOT=$tmp/lifecycle
base_path=$PATH
platform=x86
# The command's PATH for this backend on this platform fixture.
use() {
backend=$1 platform=$2
export OMARCHY_PROC_ROOT=$tmp/$platform/proc
if [[ $backend == "fake" ]]; then
export PATH="$tmp/$platform/bin:$tmp/bin:$ROOT/bin:$base_path"
else
export PATH="$tmp/$platform/bin:$tmp/real:$tmp/bin:$ROOT/bin:$base_path"
fi
}
data=$tmp/dev/data
# The slot the key opens, or nothing.
opens() {
local device=$1 key=$2 slot k
if [[ $backend == "fake" ]]; then
while IFS=$'\t' read -r slot k; do
[[ $k == "$key" ]] && { echo "$slot"; return; }
done <"$device.slots"
else
LC_ALL=C "$REAL_CRYPTSETUP" open --test-passphrase --verbose --key-file <(printf '%s' "$key") "$device" 2>&1 |
grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' || true
fi
}
account() {
awk -F'\t' -v n="$1" '$1 == n { print $2 }' "$tmp/accounts"
}
volume() {
local device=$1 key=$2 extra=${3:-}
echo "uuid-$(basename "$device")" >"$device.uuid"
if [[ $backend == "fake" ]]; then
: >"$device"
printf '0\t%s\n' "$key" >"$device.slots"
[[ -z $extra ]] || printf '1\t%s\n' "$extra" >>"$device.slots"
else
rm -f "$device"
truncate -s 32M "$device"
"$REAL_CRYPTSETUP" luksFormat -q --type "$backend" --pbkdf pbkdf2 --pbkdf-force-iterations 1000 "$device" <(printf '%s' "$key")
if [[ -n $extra ]]; then
"$REAL_CRYPTSETUP" luksAddKey --pbkdf pbkdf2 --pbkdf-force-iterations 1000 --key-file <(printf '%s' "$key") "$device" <(printf '%s' "$extra")
fi
fi
}
# / on the system drive, which also holds a recovery key; a data drive beside it.
fixture() {
rm -rf "$tmp/state" "$tmp/output" "$tmp/trace" "$tmp/offered" "$tmp"/dev/* "$tmp/slot-record" "$tmp/record-fail" "$tmp/owner-slot" "$tmp/owner-query-fail" "$tmp/owner-query-garbage" "$tmp/owner-unrecorded"
unset TEST_ENROLL_AT_PROMPT TEST_ENROLL_DEVICE TEST_OPEN_FAIL TEST_CHANGE_FAIL TEST_CHANGE_PARTIAL TEST_KILL_FAIL TEST_FINDMNT_FAIL TEST_TOKEN_SLOT CRASH_ORPHAN
system=$tmp/dev/system
recovery=${1-$recovery_key}
volume "$system" "$old_password" "$recovery"
volume "$data" "$data_password"
printf 'owner\t%s\nroot\t%s\n' "$old_password" "$old_password" >"$tmp/accounts"
printf '%s\n%s\n' "$system" "$data" >"$tmp/drives"
printf '/dev/mapper/root crypt btrfs\n%s part crypto_LUKS\n/dev/fake-disk disk \n' "$system" >"$tmp/root-ancestry"
echo "$system" >"$tmp/select"
: >"$tmp/prompts"
: >"$tmp/sudo-calls"
}
# One run of the command: an optional crash step, then answers for its prompts.
attempt() {
local crash_at=$1
shift
if (( $# )); then printf '%s\n' "$@" >"$tmp/inputs"; else : >"$tmp/inputs"; fi
echo 0 >"$tmp/steps"
echo "$crash_at" >"$tmp/crash-at"
: >"$tmp/trace"
{
CRASH_AT=$crash_at bash -c 'TEST_PID=$$ exec bash ${TEST_TRACE:+-x} "$0"' "$ROOT/bin/omarchy-drive-password"
} >>"$tmp/output" 2>&1
}
consistent() {
local context=$1 password=$2 other=$old_password
[[ $password == "$old_password" ]] && other=$new_password
untouched_accounts "$context"
[[ -n $(opens "$system" "$password") ]] || fail "$backend: $context: the disk opens with that password" "$(cat "$tmp/output")"
[[ -z $(opens "$system" "$other") ]] || fail "$backend: $context: the other password no longer opens the disk" "$(cat "$tmp/trace" "$tmp/output")"
[[ -z $recovery || -n $(opens "$system" "$recovery") ]] || fail "$backend: $context: the recovery key still opens the disk"
[[ ! -e $journal ]] || fail "$backend: $context: the journal is gone" "$(cat "$journal")"
recorded "$context" "$password"
}
# On Apple the boot package holds the slot the owner's password opens: recorded
# whenever the key moved. x86 records nothing and runs no extra sudo.
recorded() {
local context=$1 password=$2 slot
if [[ $platform == "apple" ]]; then
slot=$(opens "$system" "$password")
if [[ -s $tmp/slot-record ]]; then
[[ $(tail -n 1 "$tmp/slot-record") == "owner=$slot" ]] ||
fail "$backend: apple: $context: the owner's slot is recorded" "$(cat "$tmp/slot-record")"
else
[[ $slot == "0" ]] || fail "$backend: apple: $context: a key that moved is recorded" "$(cat "$tmp/trace")"
fi
else
[[ ! -e $tmp/slot-record ]] && ! grep -q 'lifecycle-dispatch' "$tmp/sudo-calls" ||
fail "$backend: x86: $context: no slot is recorded and no dispatch runs under sudo"
fi
}
# The disk password never changes the login or root password.
untouched_accounts() {
[[ $(account owner) == "$old_password" && $(account root) == "$old_password" ]] && ! grep -q 'chpasswd' "$tmp/sudo-calls" ||
fail "$backend: $1: the login and root passwords stay as they were" "$(cat "$tmp/accounts" "$tmp/sudo-calls")"
}
said() {
grep -Fq "$1" "$tmp/output" || fail "$backend: the command says: $1" "$(cat "$tmp/output")"
}
no_secrets() {
local file
for file in "$journal" "$tmp/sudo-calls" "$tmp/prompts"; do
[[ -e $file ]] || continue
! grep -Fq -e "$old_password" -e "$new_password" -e "$recovery_key" -e "$data_password" "$file" ||
fail "$backend: $1: $(basename "$file") holds no password" "$(cat "$file")"
done
}
# Between runs the disk opens with the old or the new password, and the
# accounts never change.
recoverable() {
local context=$1
[[ -n $(opens "$system" "$old_password") || -n $(opens "$system" "$new_password") ]] ||
fail "$backend: $context: the disk opens with the old or the new password" "$(cat "$tmp/trace")"
untouched_accounts "$context"
[[ ! -e $journal || $(stat -c %a "$journal") == "600" ]] || fail "$backend: $context: the journal is private"
no_secrets "$context"
}
backends=(fake)
if REAL_CRYPTSETUP=$(command -v cryptsetup); then
export REAL_CRYPTSETUP
backends+=(luks2 luks1)
else
skip "cryptsetup is not installed; skipping the file-backed volume runs"
fi
matrix=()
for backend in "${backends[@]}"; do
matrix+=("$backend x86" "$backend apple")
done
for run_spec in "${matrix[@]}"; do
use $run_spec
fixture
attempt 0 "$old_password" "$new_password" "$new_password" || fail "$backend: changing the system disk password succeeds" "$(cat "$tmp/output")"
consistent "uninterrupted" "$new_password"
[[ $(opens "$data" "$data_password") == "0" ]] || fail "$backend: the data drive is untouched"
! grep -Fq 'login password' "$tmp/output" || fail "$backend: the command does not mention the login password" "$(cat "$tmp/output")"
no_secrets "uninterrupted"
total_steps=$(cat "$tmp/steps")
(( total_steps >= 4 )) || fail "$backend: every change is a crash point" "$(cat "$tmp/trace")"
pass "$backend: the system disk key changes and verifies, and the login and root passwords stay as they were"
fixture
if attempt 0 "not-the-password" "$new_password" "$new_password"; then fail "$backend: a wrong current password fails the command"; fi
said "That password does not open $system."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "$backend: a wrong current password changes nothing"
[[ -n $(opens "$system" "$old_password") && $(account owner) == "$old_password" ]] || fail "$backend: a wrong current password leaves the disk and accounts"
pass "$backend: a wrong current password is refused before anything changes"
fixture ""
attempt 0 "$old_password" "$new_password" "$new_password" || fail "$backend: a single-slot system disk changes" "$(cat "$tmp/output")"
consistent "single slot" "$new_password"
pass "$backend: a disk with one key slot keeps it through the change"
fixture
echo "$data" >"$tmp/select"
attempt 0 "$data_password" "$new_password" "$new_password" || fail "$backend: changing a data drive password succeeds" "$(cat "$tmp/output")"
[[ -n $(opens "$data" "$new_password") && -z $(opens "$data" "$data_password") ]] || fail "$backend: the data drive takes the new password"
untouched_accounts "data drive"
[[ -n $(opens "$system" "$old_password") ]] || fail "$backend: a data drive change leaves the system disk alone"
[[ ! -e $journal ]] || fail "$backend: a data drive change keeps no journal"
pass "$backend: changing a non-root encrypted drive's password leaves the system disk alone"
# Killed after each step, the owner runs the command again and answers with a
# password that opens the disk now: the new one, or the old one while it still
# works. Each rerun is itself killed after each of its steps until one finishes.
for (( step = 1; step <= total_steps; step++ )); do
for answer in "$new_password" "$old_password"; do
fixture
if attempt "$step" "$old_password" "$new_password" "$new_password"; then fail "$backend: the run is killed at step $step"; fi
point=$(sed -n "${step}p" "$tmp/trace")
recoverable "killed after '$point'"
if [[ ! -e $journal ]]; then
if [[ -n $(opens "$system" "$new_password") ]]; then
consistent "killed after '$point'" "$new_password"
else
consistent "killed after '$point'" "$old_password"
fi
continue
fi
[[ -n $(opens "$system" "$answer") ]] || continue
if grep -qx 'phase=record' "$journal" && [[ $answer == "$old_password" ]]; then
if attempt 0 "$answer"; then fail "$backend: after '$point' a confirmed change refuses the old password"; fi
[[ -e $journal ]] || fail "$backend: after '$point' a refused rerun keeps the journal"
answer=$new_password
fi
for (( again = 1; ; again++ )); do
status=0
attempt "$again" "$answer" || status=$?
(( status == 0 )) && break
(( status == 137 )) || fail "$backend: the rerun after '$point' finishes" "$(cat "$tmp/output")"
recoverable "rerun after '$point' killed at step $again"
done
consistent "rerun with $answer after '$point'" "$answer"
no_secrets "rerun after '$point'"
done
done
pass "$backend $platform: killed after each of $total_steps steps, and each rerun after each of its own, the disk ends on one password and the accounts never change"
done
use fake x86
fixture
printf '%s\n%s\n%s\n' "$system" "$data" "$data" >"$tmp/drives"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a fresh boot finds the system disk without a blkid cache" "$(cat "$tmp/output")"
[[ $(cat "$tmp/offered") == "$system"$'\n'"$data" ]] || fail "each LUKS drive lsblk names is offered once" "$(cat "$tmp/offered")"
consistent "fresh boot" "$new_password"
pass "before root has run blkid, every LUKS drive is offered once and the system disk changes"
fixture
printf '5\t%s\n' tpm-sealed-key >>"$system.slots"
export TEST_TOKEN_SLOT=5
if attempt 0 "not-the-password" "$new_password" "$new_password"; then fail "an enrolled token does not stand in for the current password"; fi
said "That password does not open $system."
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a disk with an enrolled token changes its password" "$(cat "$tmp/output")"
consistent "beside an enrolled token" "$new_password"
[[ $(opens "$system" tpm-sealed-key) == "5" ]] || fail "the token's slot is left alone"
pass "an enrolled token never answers for a password, and its slot is left alone"
fixture
export TEST_CHANGE_FAIL=1
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a failed luksChangeKey fails the command"; fi
[[ -n $(opens "$system" "$old_password") && ! -e $journal ]] || fail "a failed luksChangeKey leaves the disk and no journal"
said "The disk password did not change."
pass "a failed LUKS change leaves the disk on its old password"
fixture
export TEST_CHANGE_PARTIAL=1
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a luksChangeKey that dies after adding the new key is finished" "$(cat "$tmp/output")"
consistent "luksChangeKey died after adding the new key" "$new_password"
pass "a luksChangeKey that dies between keys has its old key retired"
fixture
export TEST_CHANGE_PARTIAL=1 TEST_KILL_FAIL=1
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a failed retirement fails the command"; fi
[[ -e $journal ]] || fail "a failed retirement keeps the journal"
unset TEST_CHANGE_PARTIAL TEST_KILL_FAIL
if attempt 0 "$recovery_key"; then fail "an unconfirmed change refuses the recovery key"; fi
said "That password does not finish the change"
[[ -e $journal && -n $(opens "$system" "$recovery_key") ]] || fail "refusing the recovery key changes nothing"
attempt 0 "$new_password" || fail "the rerun retires the old key and finishes" "$(cat "$tmp/output")"
consistent "rerun after a failed retirement" "$new_password"
pass "before the change is confirmed a rerun refuses the recovery key and retires the old key"
fixture
export TEST_CHANGE_PARTIAL=1 TEST_KILL_FAIL=1
attempt 0 "$old_password" "$new_password" "$new_password" || true
unset TEST_CHANGE_PARTIAL TEST_KILL_FAIL
attempt 0 "$old_password" || fail "the rerun with the old password rolls back" "$(cat "$tmp/output")"
consistent "rollback with the old password" "$old_password"
pass "a rerun with the old password removes the half-added new key"
# On Apple the slot record follows the confirmed change: a crash there leaves
# the confirmed phase.
use fake apple
fixture
attempt 0 "$old_password" "$new_password" "$new_password"
record_step=$(grep -n 'slots recorded' "$tmp/trace" | cut -d: -f1)
fixture
attempt "$record_step" "$old_password" "$new_password" "$new_password" || true
grep -qx 'phase=record' "$journal" || fail "a crash at the slot record leaves the confirmed phase" "$(cat "$journal" "$tmp/trace")"
if attempt 0 "$recovery_key"; then fail "a confirmed change refuses the recovery key"; fi
said "That password does not finish the change"
[[ -e $journal ]] || fail "refusing the recovery key keeps the journal"
if attempt 0 "not-the-password"; then fail "a rerun refuses a password that opens nothing"; fi
for suffix in "" .slots .uuid; do
mv "$system$suffix" "$tmp/dev/renumbered$suffix"
done
system=$tmp/dev/renumbered
printf '/dev/mapper/root crypt btrfs\n%s part crypto_LUKS\n/dev/fake-disk disk \n' "$system" >"$tmp/root-ancestry"
attempt 0 "$new_password" || fail "the confirmed change finishes on a renumbered disk" "$(cat "$tmp/output")"
consistent "rerun after a confirmed change" "$new_password"
pass "apple: once the new key is confirmed only its slot finishes the change, on the disk found by UUID"
fixture
export TEST_OPEN_FAIL=1
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a disk that cannot be checked fails the command"; fi
said "Could not check the password against $system."
! grep -q 'luksChangeKey' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "a disk that cannot be checked is not changed"
pass "the current password is checked before anything changes"
fixture
attempt "$record_step" "$old_password" "$new_password" "$new_password" || true
export TEST_FINDMNT_FAIL=1
if attempt 0 "$new_password"; then fail "a run that cannot find / fails"; fi
said "Could not tell which drive holds the system."
[[ -e $journal ]] || fail "a run that cannot find / keeps the journal"
unset TEST_FINDMNT_FAIL
attempt 0 "$new_password" || fail "the rerun finishes once / is found" "$(cat "$tmp/output")"
consistent "rerun after a failed root lookup" "$new_password"
pass "apple: a failed root lookup changes nothing and keeps the journal"
fixture
attempt "$record_step" "$old_password" "$new_password" "$new_password" || true
sed -i 's/^phase=record$/phase=accounts/' "$journal"
grep -qx 'phase=accounts' "$journal" || fail "the journal is rewritten as an earlier version wrote it" "$(cat "$journal")"
if attempt 0 "$old_password"; then fail "an earlier version's confirmed change refuses the old password"; fi
attempt 0 "$new_password" || fail "an earlier version's confirmed change finishes" "$(cat "$tmp/output")"
consistent "journal from an earlier version" "$new_password"
pass "apple: a confirmed change journaled by an earlier version records its slot and leaves the accounts alone"
use fake x86
fixture
attempt 0 "$old_password" "$new_password" "$new_password"
change_step=$(grep -n 'cryptsetup luksChangeKey' "$tmp/trace" | cut -d: -f1)
fixture
export CRASH_ORPHAN=1
attempt "$change_step" "$old_password" "$new_password" "$new_password" || true
unset CRASH_ORPHAN
if attempt 0 "$old_password"; then fail "a rerun waits for a luksChangeKey the killed run left behind"; fi
attempt 0 "$new_password" || fail "the rerun finishes after the orphaned change" "$(cat "$tmp/output")"
consistent "orphaned luksChangeKey" "$new_password"
pass "a luksChangeKey that outlives its run finishes before the rerun reads the disk"
fixture
printf '/dev/mapper/root crypt btrfs\n%s part \n/dev/fake-disk disk \n' "$system" >"$tmp/root-ancestry"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a crypt layer without a named LUKS partition fails"; fi
said "Could not tell which drive holds the system."
! grep -q 'luksChangeKey' "$tmp/sudo-calls" || fail "an unresolved crypt layer changes nothing"
pass "a crypt layer whose LUKS partition cannot be named fails closed"
fixture
mkdir -p "${journal%/*}"
printf 'uuid=uuid-system\nold_slot=0\nslots=\nphase=luks\n' >"$journal"
if attempt 0 "$recovery_key"; then fail "a journal without slots refuses to finish"; fi
said "is unreadable"
[[ -e $journal && -n $(opens "$system" "$old_password") ]] || fail "an unreadable journal changes nothing"
pass "a journal without its slot list is refused rather than trusted"
fixture
export TEST_CHANGE_PARTIAL=1 TEST_KILL_FAIL=1
attempt 0 "$old_password" "$new_password" "$new_password" || true
unset TEST_CHANGE_PARTIAL TEST_KILL_FAIL
printf '3\tlater-key\n' >>"$system.slots"
if attempt 0 "$new_password"; then fail "a disk that gained two slots refuses to finish"; fi
said "changed since the interrupted change"
[[ -e $journal && -n $(opens "$system" "later-key") && -n $(opens "$system" "$old_password") ]] || fail "a disk that gained two slots is left alone"
pass "a rerun never removes a slot enrolled after the interrupted change"
fixture
mkdir -p "${journal%/*}"
exec {held}>"${journal%/*}/drive-password.lock"
flock -n "$held"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a second run waits for the first"; fi
exec {held}>&-
grep -Fq 'Another drive password change is running.' "$tmp/output" && ! grep -q 'luksChangeKey' "$tmp/sudo-calls" ||
fail "a second run changes nothing while another holds the lock"
pass "two runs never interleave"
fixture
export TEST_TRACE=1
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a traced run succeeds" "$(cat "$tmp/output")"
unset TEST_TRACE
! grep -Fq -e "$old_password" -e "$new_password" "$tmp/output" || fail "tracing never shows a password" "$(cat "$tmp/output")"
pass "bash -x does not trace passwords"
fixture
printf '/dev/mapper/root lvm btrfs\n/dev/mapper/cryptlvm crypt LVM2_member\n%s part crypto_LUKS\n/dev/fake-disk disk \n' "$system" >"$tmp/root-ancestry"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "an LVM-on-LUKS system disk changes" "$(cat "$tmp/output")"
consistent "LVM on LUKS" "$new_password"
pass "the system disk is found beneath an LVM volume group"
fixture
printf '/dev/mapper/root part ext4\n/dev/fake-disk disk \n' >"$tmp/root-ancestry"
echo "$data" >"$tmp/drives"
attempt 0 "$data_password" "$new_password" "$new_password" || fail "an unencrypted root still changes a data drive" "$(cat "$tmp/output")"
[[ -n $(opens "$data" "$new_password") ]] || fail "an unencrypted root still changes the data drive"
untouched_accounts "unencrypted root"
pass "with / unencrypted, every encrypted drive changes alone"
fixture
mkdir -p "${journal%/*}"
printf 'uuid=uuid-elsewhere\nphase=record\nslot=0\n' >"$journal"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a journal for another disk does not block a change" "$(cat "$tmp/output")"
consistent "stale journal" "$new_password"
pass "a journal for a disk that is not / is dropped"
fixture
if attempt 0 "$old_password" ""; then fail "an empty password is refused"; fi
said "Password cannot be empty."
if attempt 0 "$old_password" "secret123" "*"; then fail "a mismatched confirmation is refused"; fi
said "Passwords do not match."
if attempt 0 "$old_password" "$old_password" "$old_password"; then fail "the current password is refused as the new one"; fi
said "The new password is the current one."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] ||
fail "refused passwords change nothing"
pass "drive password rejects empty, mismatched and unchanged passphrases before changing anything"
# A change interrupted part way is finished from the one slot the password
# opened, so a system disk whose password is in two slots is refused before
# anything changes: the other copy would outlive the change.
use fake x86
fixture "$old_password"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a password in two slots is refused"; fi
said "That password also opens key slot 1 on $system"
said "The system disk password did not change."
! grep -q 'luksChangeKey' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "a password in two slots changes nothing"
[[ $(opens "$system" "$old_password") == "0" ]] && grep -qxF $'1\t'"$old_password" "$system.slots" || fail "both copies are left as they were"
pass "a system disk whose password opens two key slots is refused before anything changes"
# A change journaled by an earlier version did not check the old password for
# other slots. With other slots on the disk a copy of it may have outlived the
# change, and no password entered now can show whose slot is whose, so it is
# left for the owner to settle by hand, in every phase. With one slot it settles.
for legacy_phase in luks record accounts; do
fixture ""
printf '0\t%s\n2\t%s\n' "$new_password" "$old_password" >"$system.slots"
mkdir -p "${journal%/*}"
printf 'uuid=uuid-system\nold_slot=0\nslots=0,2\nslot=0\nphase=%s\n' "$legacy_phase" >"$journal"
if attempt 0 "$new_password" "$old_password"; then fail "an earlier version's $legacy_phase change on a disk with other slots is not settled"; fi
said "may still hold a copy of the old password"
said "Settle it by hand"
! grep -q 'luksKillSlot\|luksChangeKey' "$tmp/sudo-calls" && [[ -e $journal && -n $(opens "$system" "$old_password") ]] ||
fail "an earlier version's $legacy_phase change is left as it was" "$(cat "$tmp/sudo-calls")"
done
fixture ""
printf '0\t%s\n' "$new_password" >"$system.slots"
mkdir -p "${journal%/*}"
printf 'uuid=uuid-system\nold_slot=0\nslots=0\nphase=luks\n' >"$journal"
attempt 0 "$new_password" || fail "an earlier version's change on a one-slot disk settles" "$(cat "$tmp/output")"
[[ ! -e $journal && $(opens "$system" "$new_password") == "0" ]] || fail "the one-slot change is settled"
pass "an earlier version's interrupted change is left to the owner when other slots remain, and settles on a one-slot disk"
# The new key's slot is how the change is followed, so a new password that
# already opens a slot (another key on the disk) is refused.
fixture
if attempt 0 "$old_password" "$recovery_key" "$recovery_key"; then fail "a new password that opens a slot already is refused"; fi
said "That password already opens a key slot on $system. Choose another."
! grep -q 'luksChangeKey' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "an enrolled new password changes nothing"
echo "$data" >"$tmp/select"
volume "$data" "$data_password" "$new_password"
if attempt 0 "$data_password" "$new_password" "$new_password"; then fail "a data drive refuses a new password it already has"; fi
said "That password already opens a key slot on $data. Choose another."
pass "a new password that already opens a key slot is refused, on the system disk and data drives"
# Only the owner's slot changes the system disk. A second key an earlier setup
# added (a recovery key) is refused before anything changes, and so is a change
# the boot package cannot vouch for.
use fake apple
fixture
if attempt 0 "$recovery_key" "$new_password" "$new_password"; then fail "apple: a key in another slot is refused"; fi
said "That password opens another key slot on the system disk"
said "The system disk password did not change."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal && ! -e $tmp/slot-record ]] || fail "apple: refusing another slot's key changes nothing"
[[ -n $(opens "$system" "$recovery_key") ]] || fail "apple: the other key still opens the disk"
consistent "another slot's key refused" "$old_password"
touch "$tmp/owner-query-fail"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "apple: a boot package that cannot name the owner's slot stops the change"; fi
said "Could not read which key slot holds the system disk password: The LUKS header has no key in the recorded owner slot 0."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "apple: an unanswered owner query changes nothing"
consistent "owner query failed" "$old_password"
rm "$tmp/owner-query-fail"
touch "$tmp/owner-query-garbage"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "apple: an owner slot that is not a number stops the change"; fi
said "named 'slot zero', which is not a key slot"
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "apple: a garbled owner slot changes nothing"
rm "$tmp/owner-query-garbage"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "apple: the owner's password changes the disk" "$(cat "$tmp/output")"
consistent "owner's password after refusals" "$new_password"
pass "apple: only the owner's slot changes the system disk; another slot's key and an unanswered query change nothing"
# A Mac set up before its boot package recorded slots has no owner slot (4).
# Its disk's only key is the owner's: the change goes through and records the
# new slot. With another key beside it the owner's can't be told, so the change
# is refused, saying how to settle it: record both, or remove the extra key.
use fake apple
fixture ""
touch "$tmp/owner-unrecorded"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "apple: an unrecorded single-key disk changes its password" "$(cat "$tmp/output")"
[[ $(cat "$tmp/slot-record") == "owner=$(opens "$system" "$new_password")" ]] || fail "apple: the owner's new slot is recorded" "$(cat "$tmp/slot-record")"
consistent "no owner slot recorded, one key" "$new_password"
step=$(grep -n 'cryptsetup' "$tmp/trace" | tail -1 | cut -d: -f1)
fixture ""
touch "$tmp/owner-unrecorded"
if attempt "$step" "$old_password" "$new_password" "$new_password"; then fail "apple: the unrecorded change is killed before the slot record"; fi
[[ -e $journal && ! -e $tmp/slot-record ]] || fail "apple: a change killed before the slot record keeps its journal" "$(cat "$tmp/trace")"
attempt 0 "$new_password" || fail "apple: the rerun finishes an unrecorded change" "$(cat "$tmp/output")"
[[ $(cat "$tmp/slot-record") == "owner=$(opens "$system" "$new_password")" ]] || fail "apple: the rerun records the owner's new slot" "$(cat "$tmp/slot-record")"
consistent "no owner slot recorded, interrupted" "$new_password"
pass "apple: with no owner slot recorded, the disk's only key changes and its new slot is recorded, after an interruption too"
fixture
touch "$tmp/owner-unrecorded"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "apple: an unrecorded disk with two keys refuses the change"; fi
said "has keys in slots 0,1, so which one is yours can't be told"
said "record both: sudo omarchy-lifecycle-dispatch luks-slots owner=0 recovery=1"
said "sudo cryptsetup luksKillSlot $system <slot>"
said "The system disk password did not change."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal && ! -e $tmp/slot-record ]] || fail "apple: an unrecorded two-key disk changes nothing"
"$ROOT/bin/omarchy-lifecycle-dispatch" luks-slots owner=0 recovery=1
: >"$tmp/slot-record"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "apple: once both slots are recorded the change goes through" "$(cat "$tmp/output")"
consistent "slots recorded as the refusal said" "$new_password"
pass "apple: with no owner slot recorded, a disk with another key refuses the change and says how to record or remove it"
fixture ""
touch "$tmp/owner-unrecorded"
export TEST_ENROLL_AT_PROMPT="Confirm new encryption password" TEST_ENROLL_DEVICE=$system
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "apple: a key enrolled during the prompts stops an unrecorded change"; fi
said "changed while you entered the new password"
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal && ! -e $tmp/slot-record ]] || fail "apple: a key enrolled during the prompts changes nothing"
pass "apple: an unrecorded disk that gains a key while the owner types is left alone"
# A slot the boot package could not record keeps the journal: the rerun records it.
use fake apple
fixture
touch "$tmp/record-fail"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "a failed record fails the command"; fi
said "Could not record the new key slot for the boot checks."
[[ -e $journal && ! -e $tmp/slot-record ]] || fail "a failed record keeps the journal"
rm "$tmp/record-fail"
attempt 0 "$new_password" || fail "the rerun records the slot" "$(cat "$tmp/output")"
[[ -s $tmp/slot-record ]] || fail "the rerun records the moved key's slot"
consistent "rerun after a failed record" "$new_password"
pass "apple: the change finishes only once the boot package recorded the owner's new slot"
# An installed boot package older than luks-slots could not record the slot, so
# the system disk does not change; a data drive still does.
fixture
mv "$tmp/lifecycle/usr/lib/omarchy/mac-boot/luks-slots" "$tmp/luks-slots.off"
mkdir -p "$tmp/lifecycle/var/lib/pacman/local/omarchy-mac-boot-20260925-2"
if attempt 0 "$old_password" "$new_password" "$new_password"; then fail "an Apple boot package without luks-slots fails the command"; fi
said "which omarchy-mac-boot 20260925-2 does not provide; update omarchy-mac-boot"
said "The system disk password did not change."
! grep -q 'luksChangeKey\|chpasswd' "$tmp/sudo-calls" && [[ ! -e $journal ]] || fail "a boot package without luks-slots changes nothing"
consistent "boot package without luks-slots" "$old_password"
echo "$data" >"$tmp/select"
attempt 0 "$data_password" "$new_password" "$new_password" || fail "a data drive changes without luks-slots" "$(cat "$tmp/output")"
pass "apple: a boot package without luks-slots stops a system disk change before it starts, naming the package"
# A Mac without its boot package at all predates it: nothing checks its slots,
# so the change goes through and records nothing.
fixture
rm -r "$tmp/lifecycle/var/lib/pacman"
attempt 0 "$old_password" "$new_password" "$new_password" || fail "a Mac without the boot package changes its disk password" "$(cat "$tmp/output")"
# Checked as on x86: no slot recorded and no dispatch under sudo.
platform=x86
consistent "no boot package" "$new_password"
platform=apple
! grep -q 'Error:' "$tmp/output" || fail "without the boot package nothing is reported" "$(cat "$tmp/output")"
mv "$tmp/luks-slots.off" "$tmp/lifecycle/usr/lib/omarchy/mac-boot/luks-slots"
pass "apple: a Mac without its boot package changes its disk password and records no slot"
fixture
echo "$data" >"$tmp/select"
attempt 0 "$data_password" "$new_password" "$new_password" || fail "apple: a data drive changes" "$(cat "$tmp/output")"
[[ ! -e $tmp/slot-record && -n $(opens "$data" "$new_password") ]] || fail "apple: a data drive records no slot"
pass "apple: only the system disk's slot is recorded"
+339
View File
@@ -0,0 +1,339 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Factory reset's boot step through the real omarchy-lifecycle-dispatch. On an
# x86 fixture the generic Limine UKI path runs and the boot-package entrypoints
# on disk never do. On an Apple fixture a fake boot package owns the factory
# root's boot chain: it prepares and verifies it before the throwaway slot is
# added, gets the key on standard input after the switch, and rolls back when
# the reset fails before the switch. Subvolumes are directories, and
# cryptsetup is a slot-table fake.
require_platform_fixtures "factory reset through lifecycle dispatch"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
fake_platform "$tmp/x86" x86
fake_platform "$tmp/apple" aarch64-apple
# Load the production functions without self-elevation or the reset entrypoint.
awk '
/^[a-z_]+\(\) \{/ { copying = 1 }
copying { print }
/^}/ { copying = 0 }
' "$ROOT/bin/omarchy-system-factory-reset" >"$tmp/functions"
# The fake boot package: each entrypoint records its call, reset-commit its
# standard input, and reset-verify fails on request.
lifecycle=$tmp/lifecycle
mac_boot=$lifecycle/usr/lib/omarchy/mac-boot
mkdir -p "$mac_boot"
for operation in reset-prepare reset-verify reset-commit reset-rollback; do
cat >"$mac_boot/$operation" <<SH
#!/bin/bash
echo "$operation\${*:+ \$*}" >>"$tmp/calls"
[[ $operation != reset-commit ]] || cat >"$tmp/committed-key"
[[ ! -e $tmp/$operation-fail ]] || { echo "$operation: the fixture refuses" >&2; exit 1; }
SH
done
chmod 755 "$mac_boot"/*
chmod -R go-w "$lifecycle"
cat >"$tmp/reset" <<'SH'
#!/bin/bash
set -euo pipefail
source "$TMP/functions"
TOP_MNT=$TMP/top
NEXT_NAME=@omarchy-reset-next
PROVISIONING_DIR=/var/lib/omarchy/provisioning
LOG_FILE=$TMP/reset.log
DISPATCH=${TEST_DISPATCH:-$ROOT/bin/omarchy-lifecycle-dispatch}
RESET_BOOT=""
RESET_BOOT_ERROR=""
swap_done=0
gum() {
if [[ $1 == "input" ]]; then
printf '%s\n' "$CURRENT"
elif [[ $1 == "style" ]]; then
printf '%s\n' "${!#}" >>"$TMP/screen"
fi
}
encrypted_install() { [[ -n $DEVICE ]]; }
luks_device() { echo "$DEVICE"; }
scrub_factory_accounts() { :; }
sanitize_factory_baseline() { :; }
install_provisioning_units() { :; }
systemd-id128() { printf '%032d\n' 7; }
sync() { :; }
mountpoint() { return 0; }
mount() { echo "mount $*" >>"$TMP/calls"; }
umount() { :; }
chroot() { echo "chroot ${*:2}" >>"$TMP/calls"; }
reset_limine_config() { echo "limine menu reset" >>"$TMP/calls"; }
verify_limine_hashes() { :; }
btrfs() {
case "$1 $2" in
"subvolume snapshot") cp -a "$3" "$4" ;;
"subvolume delete") rm -rf "${!#}" ;;
"subvolume show") return 1 ;;
esac
}
mv() {
if [[ -e $TMP/switch-fail && $1 == "$TOP_MNT/@" ]]; then
echo "mv: the fixture refuses the switch" >&2
return 1
fi
if [[ -e $TMP/activate-fail && $1 == "$TOP_MNT/$NEXT_NAME" ]]; then
echo "mv: the fixture refuses the activation" >&2
return 1
fi
if [[ -e $TMP/restore-fail && $1 == "$TOP_MNT"/@omarchy-old-* ]]; then
echo "mv: the fixture refuses the restore" >&2
return 1
fi
command mv "$@"
}
# "<slot> <key>" per line.
cryptsetup() {
local op=$1 key_file="" slot material next
local -a args=()
shift
while (( $# )); do
case $1 in
--key-file) key_file=$2; shift 2 ;;
--token-type) shift 2 ;;
-*) shift ;;
*) args+=("$1"); shift ;;
esac
done
echo "cryptsetup $op" >>"$TMP/calls"
case $op in
open)
material=$(cat "$key_file")
if [[ -e $TMP/lookup-fail-once && $material != "$CURRENT" ]]; then
rm "$TMP/lookup-fail-once"
return 2
fi
slot=$(awk -v m="$material" '$2 == m { print $1; exit }' "$TMP/slots")
[[ -n $slot ]] || return 2
echo "Key slot $slot unlocked."
;;
luksAddKey)
material=$(cat "$key_file")
awk -v m="$material" '$2 == m { found = 1 } END { exit !found }' "$TMP/slots" || return 2
for (( next = 0; next < 32; next++ )); do
awk '{ print $1 }' "$TMP/slots" | grep -qx "$next" || break
done
printf '%s %s\n' "$next" "$(cat "${args[1]}")" >>"$TMP/slots"
;;
luksDump)
echo "Keyslots:"
awk '{ printf " %s: luks2\n", $1 }' "$TMP/slots"
;;
luksKillSlot)
slot=${args[1]}
awk -v s="$slot" '$1 != s' "$TMP/slots" >"$TMP/slots.next"
command mv -f "$TMP/slots.next" "$TMP/slots"
;;
*) return 1 ;;
esac
}
case $MODE in
owner) reset_boot_owner && echo "$RESET_BOOT" ;;
reset)
trap cleanup EXIT
stage_full_reset
;;
esac
SH
current_password=owner-password
device=$tmp/luks-device
# A top-level Btrfs volume: the running root and a factory baseline whose ESP
# the generic path mounts from its fstab.
fixture() {
rm -rf "$tmp/top" "$tmp/calls" "$tmp/screen" "$tmp/reset.log" "$tmp/committed-key" "$tmp"/*-fail
mkdir -p "$tmp/top/@" "$tmp/top/@factory/etc" "$tmp/top/@factory/usr/bin" \
"$tmp/top/@factory/usr/share/omarchy/install/provisioning" "$tmp/top/@factory/boot"
touch "$tmp/top/@/old-system" "$tmp/top/@factory/factory-system" "$tmp/esp" \
"$tmp/top/@factory/usr/share/omarchy/install/provisioning/omarchy-provision-owner.service"
printf '#!/bin/bash\n' >"$tmp/top/@factory/usr/bin/omarchy-provision-owner"
chmod +x "$tmp/top/@factory/usr/bin/omarchy-provision-owner"
printf '%s /boot vfat defaults 0 2\n' "$tmp/esp" >"$tmp/top/@factory/etc/fstab"
: >"$device"
printf '0 %s\n' "$current_password" >"$tmp/slots"
: >"$tmp/reset.log"
}
run() {
local mode=$1 platform=$2
TMP=$tmp ROOT=$ROOT MODE=$mode CURRENT=$current_password DEVICE=${DEVICE-$device} TEST_DISPATCH=${TEST_DISPATCH:-} \
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT=$lifecycle PATH="$tmp/$platform/bin:$PATH" \
bash "$tmp/reset" >"$tmp/out" 2>&1
}
activated() {
[[ -e $tmp/top/@/factory-system && -f $tmp/top/@/var/lib/omarchy/provisioning/pending ]] &&
compgen -G "$tmp/top/@omarchy-old-*/old-system" >/dev/null
}
untouched() {
[[ -e $tmp/top/@/old-system && ! -e $tmp/top/@omarchy-reset-next ]] && [[ $(cat "$tmp/slots") == "0 $current_password" ]]
}
# x86: the generic Limine UKI path, with boot-package entrypoints on disk that
# would record themselves if they ran.
fixture
run owner x86 && [[ $(cat "$tmp/out") == "generic" ]] || fail "x86: the reset boot step is generic" "$(cat "$tmp/out")"
run reset x86 || fail "x86: an encrypted reset stages" "$(cat "$tmp/out" "$tmp/reset.log")"
activated || fail "x86: the factory root is activated"
grep -qx 'chroot /usr/bin/limine-update' "$tmp/calls" || fail "x86: limine-update rebuilds the factory root's UKI" "$(cat "$tmp/calls")"
[[ -f $tmp/top/@/etc/omarchy/provisioning.key && -f $tmp/top/@/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf &&
-f $tmp/top/@/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf ]] || fail "x86: the UKI carries the reset's auto-unlock"
[[ $(awk 'NR == 2 { print $2 }' "$tmp/slots") == "$(cat "$tmp/top/@/var/lib/omarchy/provisioning/luks-key")" ]] ||
fail "x86: the throwaway slot opens with the staged key" "$(cat "$tmp/slots")"
! grep -q '^reset-' "$tmp/calls" || fail "x86: no boot-package entrypoint runs" "$(cat "$tmp/calls")"
pass "x86: dispatch is a no-op and the generic Limine UKI path resets the machine"
# A failed activation on the generic path puts the previous root back and
# revokes the throwaway slot. The ESP already holds the factory root's UKI, so
# the screen says to rebuild the boot files rather than claiming nothing changed.
fixture
touch "$tmp/activate-fail"
if run reset x86; then fail "x86: a failed activation fails the reset"; fi
untouched || fail "x86: a failed activation puts the previous root back and revokes the throwaway slot" "$(ls "$tmp/top"; cat "$tmp/slots")"
! grep -q '^reset-' "$tmp/calls" || fail "x86: a failed activation runs no boot-package entrypoint" "$(cat "$tmp/calls")"
grep -q "run 'sudo limine-update' before rebooting" "$tmp/screen" ||
fail "x86: a failed activation says the boot files need rebuilding" "$(cat "$tmp/screen")"
pass "x86: a failed activation puts the previous root back, revokes the throwaway slot and says to rebuild the boot files"
# Apple: the boot package prepares and verifies the factory root before the
# throwaway slot exists, and gets the key only once the factory root is active.
fixture
run owner apple && [[ $(cat "$tmp/out") == "platform" ]] || fail "apple: the boot package owns the reset boot step" "$(cat "$tmp/out")"
run reset apple || fail "apple: an encrypted reset stages" "$(cat "$tmp/out" "$tmp/reset.log")"
activated || fail "apple: the factory root is activated"
expected="reset-prepare $tmp/top/@omarchy-reset-next $device
reset-verify $tmp/top/@omarchy-reset-next
cryptsetup luksAddKey
reset-commit"
[[ $(grep -v '^cryptsetup \(open\|luksDump\)' "$tmp/calls") == "$expected" ]] ||
fail "apple: prepare, verify, the throwaway slot, then commit" "$(cat "$tmp/calls")"
staged=$(cat "$tmp/top/@/var/lib/omarchy/provisioning/luks-key")
[[ -n $staged && $(cat "$tmp/committed-key") == "$staged" ]] || fail "apple: reset-commit gets the staged key on standard input"
[[ $(awk 'NR == 2 { print $2 }' "$tmp/slots") == "$staged" ]] || fail "apple: the throwaway slot opens with the staged key" "$(cat "$tmp/slots")"
[[ ! -e $tmp/top/@/etc/omarchy/provisioning.key && ! -e $tmp/top/@/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf ]] ||
fail "apple: no Limine UKI unlock is written"
! grep -q 'limine' "$tmp/calls" || fail "apple: the Limine UKI path never runs" "$(cat "$tmp/calls")"
! grep -Fq "$staged" "$tmp/reset.log" "$tmp/calls" "$tmp/out" || fail "apple: the throwaway key is never logged"
pass "apple: the boot package prepares and verifies the factory root before the throwaway slot, and gets the key after the switch"
# A failed verification adds no credential and rolls the boot state back; the
# previous root keeps booting.
fixture
touch "$tmp/reset-verify-fail"
if run reset apple; then fail "apple: a failed verification fails the reset"; fi
[[ $(grep '^reset-' "$tmp/calls" | cut -d' ' -f1) == $'reset-prepare\nreset-verify\nreset-rollback' ]] ||
fail "apple: a failed verification rolls back" "$(cat "$tmp/calls")"
! grep -q 'luksAddKey' "$tmp/calls" || fail "apple: a failed verification adds no slot"
untouched || fail "apple: a failed verification leaves the previous root and its slots" "$(cat "$tmp/slots")"
grep -q 'reset-verify: the fixture refuses' "$tmp/screen" || fail "apple: the verification's reason is shown" "$(cat "$tmp/screen")"
pass "apple: a failed verification rolls back, adds no slot and keeps the previous root"
# A failure after the slot was added and before the switch revokes it too.
fixture
touch "$tmp/switch-fail"
if run reset apple; then fail "apple: a failed switch fails the reset"; fi
grep -q 'luksAddKey' "$tmp/calls" && grep -qx 'reset-rollback' "$tmp/calls" && ! grep -q '^reset-commit' "$tmp/calls" ||
fail "apple: a failed switch rolls back without committing" "$(cat "$tmp/calls")"
untouched || fail "apple: a failed switch revokes the throwaway slot and keeps the previous root" "$(cat "$tmp/slots")"
pass "apple: a failure before the switch revokes the slot it added and rolls the boot state back"
# The old root already moved aside when the factory root cannot take @: the old
# root goes back, and the reset is undone as if it failed before the switch.
fixture
touch "$tmp/activate-fail"
if run reset apple; then fail "apple: a failed activation fails the reset"; fi
grep -qx 'reset-rollback' "$tmp/calls" && ! grep -q '^reset-commit' "$tmp/calls" ||
fail "apple: a failed activation rolls back without committing" "$(cat "$tmp/calls")"
untouched || fail "apple: a failed activation puts the previous root back at @ and revokes the throwaway slot" "$(ls "$tmp/top"; cat "$tmp/slots")"
! compgen -G "$tmp/top/@omarchy-old-*" >/dev/null || fail "apple: a failed activation leaves no renamed root behind" "$(ls "$tmp/top")"
grep -q 'the current root is back at @' "$tmp/screen" || fail "apple: a failed activation says the current root is back" "$(cat "$tmp/screen")"
pass "apple: a failed activation puts the previous root back, rolls back and revokes the slot"
# When the old root cannot go back either, neither root is at @: both survive
# for recovery, and the screen names the rename that restores the machine.
fixture
touch "$tmp/activate-fail" "$tmp/restore-fail"
if run reset apple; then fail "apple: a failed activation and restore fails the reset"; fi
old=$(compgen -G "$tmp/top/@omarchy-old-*") || fail "apple: the previous root survives under its moved-aside name" "$(ls "$tmp/top")"
[[ -e $old/old-system && -e $tmp/top/@omarchy-reset-next/factory-system && ! -e $tmp/top/@ ]] ||
fail "apple: a failed restore keeps both roots" "$(ls "$tmp/top")"
grep -qx 'reset-rollback' "$tmp/calls" && [[ $(cat "$tmp/slots") == "0 $current_password" ]] ||
fail "apple: a failed restore still rolls the boot state back and revokes the slot" "$(cat "$tmp/calls" "$tmp/slots")"
grep -q "rename ${old##*/} to @" "$tmp/screen" || fail "apple: a failed restore names the recovery rename" "$(cat "$tmp/screen")"
pass "apple: a failed activation and restore keeps both roots and names the recovery rename"
# A slot added but not confirmed is found by its key and revoked all the same.
fixture
touch "$tmp/lookup-fail-once"
if run reset apple; then fail "apple: an unconfirmed throwaway slot fails the reset"; fi
grep -qx 'reset-rollback' "$tmp/calls" && grep -q 'luksKillSlot' "$tmp/calls" || fail "apple: an unconfirmed slot is rolled back and killed" "$(cat "$tmp/calls")"
untouched || fail "apple: an unconfirmed throwaway slot is revoked" "$(cat "$tmp/slots")"
pass "apple: a throwaway slot the reset could not confirm is found by its key and revoked"
# Once the factory root is active nothing rolls back: a failed commit is logged
# and costs the first boot one password prompt.
fixture
touch "$tmp/reset-commit-fail"
run reset apple || fail "apple: a failed commit still completes the reset" "$(cat "$tmp/out")"
activated && ! grep -qx 'reset-rollback' "$tmp/calls" || fail "apple: a failed commit keeps the factory root active" "$(cat "$tmp/calls")"
grep -q 'reset-commit: the fixture refuses' "$tmp/reset.log" || fail "apple: a failed commit is logged" "$(cat "$tmp/reset.log")"
[[ $(wc -l <"$tmp/slots") == 2 ]] || fail "apple: a failed commit keeps the throwaway slot"
pass "apple: a failed commit after the switch is logged and the reset stands"
# An unencrypted Mac still has its boot files rebuilt and verified.
fixture
DEVICE="" run reset apple || fail "apple: an unencrypted reset stages" "$(cat "$tmp/out")"
[[ $(grep '^reset-' "$tmp/calls") == "reset-prepare $tmp/top/@omarchy-reset-next"$'\n'"reset-verify $tmp/top/@omarchy-reset-next"$'\n'"reset-commit" ]] ||
fail "apple: an unencrypted reset names no LUKS device" "$(cat "$tmp/calls")"
! grep -q '^cryptsetup' "$tmp/calls" || fail "apple: an unencrypted reset touches no key slot"
pass "apple: an unencrypted reset rebuilds and verifies the boot files without a key"
# Without the boot package, or with one that lacks a reset operation, the
# reset stops before anything is asked or changed, naming the package.
fixture
mv "$mac_boot" "$tmp/mac-boot.off"
if run owner apple; then fail "apple without the boot package: the reset refuses"; fi
grep -q 'reset-prepare on aarch64-apple needs omarchy-mac-boot' "$tmp/out" || fail "apple without the boot package: the package is named" "$(cat "$tmp/out")"
mkdir -p "$mac_boot"
cp -p "$tmp/mac-boot.off/reset-prepare" "$tmp/mac-boot.off/reset-verify" "$mac_boot/"
chmod -R go-w "$lifecycle"
run owner x86 && [[ $(cat "$tmp/out") == "generic" ]] || fail "x86: a partial boot package on disk is ignored" "$(cat "$tmp/out")"
if run owner apple; then fail "apple with part of the boot package: the reset refuses"; fi
grep -q 'reset-commit on aarch64-apple needs omarchy-mac-boot' "$tmp/out" || fail "apple with part of the boot package: the missing operation is named" "$(cat "$tmp/out")"
rm -rf "$mac_boot"
mv "$tmp/mac-boot.off" "$mac_boot"
pass "apple: without the boot package's reset operations, or with only some of them, the reset refuses naming the package"
# A registration that left the reset operations optional and got only some of
# them would mix two boot paths: the reset refuses that too.
mkdir -p "$tmp/half"
cat >"$tmp/half/omarchy-lifecycle-dispatch" <<SH
#!/bin/bash
[[ \$* != "--resolve reset-prepare" ]] || echo /usr/lib/omarchy/mac-boot/reset-prepare
SH
chmod +x "$tmp/half/omarchy-lifecycle-dispatch"
fixture
if TEST_DISPATCH=$tmp/half/omarchy-lifecycle-dispatch run owner apple; then fail "a partial set of reset operations is refused"; fi
grep -q 'implements only some of the factory reset operations' "$tmp/out" || fail "a partial set of reset operations is explained" "$(cat "$tmp/out")"
pass "a boot package implementing only some of the reset operations is refused"
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
if (( EUID == 0 )); then
skip "factory-reset self-elevation requires an unprivileged caller"
exit 0
fi
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/bin" "$tmp/checkout with spaces"
# Exercise the real startup, but never include the destructive reset body.
awk '
/^export PATH=/ { found = 1; print "exit 99"; exit }
{ print }
END { if (!found) exit 1 }
' "$ROOT/bin/omarchy-system-factory-reset" >"$tmp/checkout with spaces/reset"
grep -Fxq 'PACKAGED_PATH=/usr/bin/omarchy-system-factory-reset' "$ROOT/bin/omarchy-system-factory-reset" || fail "factory reset pins the packaged path"
sed -i "s|^PACKAGED_PATH=.*|PACKAGED_PATH=\"$tmp/packaged-reset\"|" "$tmp/checkout with spaces/reset"
cp "$tmp/checkout with spaces/reset" "$tmp/packaged-reset"
chmod +x "$tmp/checkout with spaces/reset" "$tmp/packaged-reset"
ln -s "$tmp/checkout with spaces/reset" "$tmp/reset-link"
cat >"$tmp/bin/sudo" <<'SH'
#!/bin/bash
printf '%s\0' "$@" >"$CALL_LOG"
exit "${SUDO_STATUS:-0}"
SH
chmod +x "$tmp/bin/sudo"
args=('argument with spaces' '' '--flag' '$(touch should-not-run)' $'two\nlines')
expected=(env 'GUM_INPUT_PROMPT=Reset this computer? ' "$tmp/packaged-reset" "${args[@]}")
for invocation in "$tmp/checkout with spaces/reset" './checkout with spaces/reset' "$tmp/reset-link" "$tmp/packaged-reset" reset; do
for status in 0 1 127; do
actual_status=0
(cd "$tmp" && env -i HOME="$tmp" PATH="$tmp/bin:$tmp/checkout with spaces:/usr/bin:/bin" \
OMARCHY_PATH="$tmp/checkout with spaces" PACKAGED_PATH="$tmp/wrong-target" \
GUM_INPUT_PROMPT='Reset this computer? ' CALL_LOG="$tmp/call" SUDO_STATUS="$status" \
"$invocation" "${args[@]}") || actual_status=$?
(( actual_status == status )) || fail "elevation status is preserved for $invocation"
mapfile -d '' -t actual <"$tmp/call"
(( ${#actual[@]} == ${#expected[@]} )) || fail "elevation argument count is preserved"
for i in "${!expected[@]}"; do
[[ ${actual[i]} == "${expected[i]}" ]] || fail "elevation argument $i is preserved for $invocation"
done
[[ ! -e $tmp/should-not-run ]] || fail "caller arguments were executed"
done
pass "factory reset pins $invocation to the packaged command and preserves arguments, styling and status"
done
if unshare -Ur true >/dev/null 2>&1; then
status=0
unshare -Ur "$tmp/packaged-reset" || status=$?
(( status == 99 )) || fail "matching root invocation did not reach the guarded body"
pass "matching packaged root invocation reaches the reset body"
else
skip "user namespaces unavailable; root invocation coverage"
fi
# A checkout must not hand off to older installed code, even with explicit sudo.
printf '\n# older packaged copy\n' >>"$tmp/packaged-reset"
rm -f "$tmp/call"
if CALL_LOG="$tmp/call" PATH="$tmp/bin:/usr/bin:/bin" "$tmp/reset-link" >"$tmp/mismatch" 2>&1; then
fail "mismatched reset implementation was accepted"
fi
[[ ! -e $tmp/call ]] || fail "mismatched reset requested elevation"
grep -Fq 'Install the matching Omarchy package' "$tmp/mismatch" || fail "mismatch explains recovery"
if unshare -Ur true >/dev/null 2>&1; then
status=0
unshare -Ur "$tmp/reset-link" >"$tmp/root-mismatch" 2>&1 || status=$?
(( status == 1 )) || fail "root invocation did not refuse mismatch before the reset body"
pass "root checkout invocation refuses a mismatched packaged reset"
fi
pass "mismatched checkout refuses before elevation"
+35
View File
@@ -33,3 +33,38 @@ if grep -F 'skip-first-run-update-notification' "$ROOT/install/user/first-run/wi
fi
pass "first-run uses one lifecycle completion marker"
# systemctl enables none of a list when one unit in it is unknown, so a unit
# missing from a build must cost first-run only that unit.
cat >"$mock_bin/systemctl" <<'SH'
#!/bin/bash
[[ $* != *"$OMARCHY_TEST_MISSING_UNIT"* ]] || exit 1
printf '%s\n' "$*" >>"$OMARCHY_TEST_CALLS"
SH
cat >"$mock_bin/omarchy-hook-install" <<'SH'
#!/bin/bash
echo "hook $*" >>"$OMARCHY_TEST_CALLS"
SH
chmod +x "$mock_bin/systemctl" "$mock_bin/omarchy-hook-install"
enable_user_units() {
rm -f "$test_tmp/calls"
PATH="$mock_bin:$PATH" OMARCHY_TEST_CALLS="$test_tmp/calls" OMARCHY_TEST_MISSING_UNIT="$1" \
bash "$ROOT/install/user/first-run/enable-user-units.sh"
}
enable_user_units no-such-unit || fail "first-run enables its user units"
clean_run=$(<"$test_tmp/calls")
grep -Fq 'hook theme-set' <<<"$clean_run" || fail "first-run installs the theme hook"
mapfile -t units < <(sed -n 's/^--user enable --now //p' <<<"$clean_run")
for unit in "${units[@]}"; do
[[ $unit != *" "* ]] || fail "first-run enables each unit on its own" "$unit"
if enable_user_units "$unit"; then
fail "first-run reports a unit it could not enable" "$unit"
fi
[[ $(<"$test_tmp/calls") == "$(grep -Fvx -- "--user enable --now $unit" <<<"$clean_run")" ]] ||
fail "a unit missing from the build costs first-run only that unit" "$unit"
done
(( ${#units[@]} > 1 )) || fail "first-run enables each unit on its own"
pass "a unit missing from the build costs first-run only that unit"
@@ -41,12 +41,23 @@ ShellRoot {
}
}
QtObject {
id: remoteSessionService
property bool active: false
property var peers: []
property int refreshes: 0
function refresh() {
refreshes++
}
}
QtObject {
id: mockShell
function firstPartyServiceFor(id) {
if (id === "omarchy.notifications") return notificationService
if (id === "omarchy.idle") return idleService
if (id === "omarchy.nightlight") return nightlightService
if (id === "omarchy.remote-session") return remoteSessionService
return null
}
}
@@ -74,6 +85,7 @@ ShellRoot {
QtObject {
id: indicatorHost
property bool revealInactiveIndicators: true
signal refreshRequested()
}
function createIndicator(name) {
@@ -198,6 +210,19 @@ ShellRoot {
root.assertTrue(root.commandCount("omarchy-capture-screenrecording --stop-recording") === 1, "Screen Recording left click stops active recording")
}
var remoteSession = root.createIndicator("RemoteSession")
if (remoteSession) {
remoteSession.moduleName = "RemoteSession"
root.injectBar(remoteSession)
root.assertTrue(remoteSession.active === false, "Remote Session starts inactive without a gliff server")
remoteSessionService.active = true
remoteSessionService.peers = ["10.0.0.5"]
root.assertTrue(remoteSession.active === true, "Remote Session follows the service state")
root.assertTrue(remoteSession.tooltipText === "Remote session from 10.0.0.5", "Remote Session tooltip names the connected peer")
indicatorHost.refreshRequested()
root.assertTrue(remoteSessionService.refreshes === 1, "Remote Session refresh re-probes the service")
}
var stayAwake = root.createIndicator("StayAwake")
if (stayAwake) {
stayAwake.moduleName = "StayAwake"
@@ -0,0 +1,18 @@
# Generated by the Omarchy Apple Silicon image builder.
_omarchy_asahi_hooks=()
_omarchy_asahi_added=false
for _omarchy_asahi_hook in "${HOOKS[@]}"; do
if [[ $_omarchy_asahi_hook == asahi ]]; then
_omarchy_asahi_added=true
fi
if [[ $_omarchy_asahi_hook == filesystems && $_omarchy_asahi_added == false ]]; then
_omarchy_asahi_hooks+=(asahi omarchy-vendorfw)
_omarchy_asahi_added=true
fi
_omarchy_asahi_hooks+=("$_omarchy_asahi_hook")
done
if [[ $_omarchy_asahi_added == false ]]; then
_omarchy_asahi_hooks+=(asahi omarchy-vendorfw)
fi
HOOKS=("${_omarchy_asahi_hooks[@]}")
unset _omarchy_asahi_hooks _omarchy_asahi_hook _omarchy_asahi_added
@@ -0,0 +1,90 @@
# Insert omarchy-mac-encrypt after vendorfw/block and sd-encrypt immediately
# before filesystems, each only if that hook is absent. 90-omarchy-mac.conf
# already put asahi and omarchy-vendorfw before filesystems; this drop-in is
# sourced after it.
#
# Both are systemd initrd units: the systemd hook replaces udev (mkinitcpio's
# stock HOOKS line) and keymap/consolefont become sd-vconsole. A HOOKS line
# carrying the busybox encrypt hook belongs to a Mac unlocked by cryptdevice=,
# which sd-encrypt cannot parse: that line is left exactly as it is.
_omarchy_mac_encrypt_hooks=()
_omarchy_mac_encrypt_have_systemd=false
_omarchy_mac_encrypt_have_vconsole=false
_omarchy_mac_encrypt_have_encrypt=false
for _omarchy_mac_encrypt_hook in "${HOOKS[@]}"; do
case $_omarchy_mac_encrypt_hook in
systemd) _omarchy_mac_encrypt_have_systemd=true ;;
sd-vconsole) _omarchy_mac_encrypt_have_vconsole=true ;;
encrypt) _omarchy_mac_encrypt_have_encrypt=true ;;
esac
done
if [[ $_omarchy_mac_encrypt_have_encrypt == false ]]; then
for _omarchy_mac_encrypt_hook in "${HOOKS[@]}"; do
case $_omarchy_mac_encrypt_hook in
udev)
if [[ $_omarchy_mac_encrypt_have_systemd == false ]]; then
_omarchy_mac_encrypt_hooks+=(systemd)
_omarchy_mac_encrypt_have_systemd=true
fi
;;
keymap|consolefont)
if [[ $_omarchy_mac_encrypt_have_vconsole == false ]]; then
_omarchy_mac_encrypt_hooks+=(sd-vconsole)
_omarchy_mac_encrypt_have_vconsole=true
fi
;;
*) _omarchy_mac_encrypt_hooks+=("$_omarchy_mac_encrypt_hook") ;;
esac
done
if [[ $_omarchy_mac_encrypt_have_systemd == false ]]; then
if [[ ${_omarchy_mac_encrypt_hooks[0]:-} == base ]]; then
_omarchy_mac_encrypt_hooks=(base systemd "${_omarchy_mac_encrypt_hooks[@]:1}")
else
_omarchy_mac_encrypt_hooks=(systemd "${_omarchy_mac_encrypt_hooks[@]}")
fi
fi
HOOKS=("${_omarchy_mac_encrypt_hooks[@]}")
_omarchy_mac_encrypt_hooks=()
_omarchy_mac_encrypt_have_ours=false
_omarchy_mac_encrypt_have_sd=false
_omarchy_mac_encrypt_added_ours=false
_omarchy_mac_encrypt_added_sd=false
for _omarchy_mac_encrypt_hook in "${HOOKS[@]}"; do
if [[ $_omarchy_mac_encrypt_hook == omarchy-mac-encrypt ]]; then
_omarchy_mac_encrypt_have_ours=true
fi
if [[ $_omarchy_mac_encrypt_hook == sd-encrypt ]]; then
_omarchy_mac_encrypt_have_sd=true
fi
done
for _omarchy_mac_encrypt_hook in "${HOOKS[@]}"; do
if [[ $_omarchy_mac_encrypt_hook == sd-encrypt && $_omarchy_mac_encrypt_have_ours == false &&
$_omarchy_mac_encrypt_added_ours == false ]]; then
_omarchy_mac_encrypt_hooks+=(omarchy-mac-encrypt)
_omarchy_mac_encrypt_added_ours=true
fi
if [[ $_omarchy_mac_encrypt_hook == filesystems ]]; then
if [[ $_omarchy_mac_encrypt_have_ours == false && $_omarchy_mac_encrypt_added_ours == false ]]; then
_omarchy_mac_encrypt_hooks+=(omarchy-mac-encrypt)
_omarchy_mac_encrypt_added_ours=true
fi
if [[ $_omarchy_mac_encrypt_have_sd == false && $_omarchy_mac_encrypt_added_sd == false ]]; then
_omarchy_mac_encrypt_hooks+=(sd-encrypt)
_omarchy_mac_encrypt_added_sd=true
fi
fi
_omarchy_mac_encrypt_hooks+=("$_omarchy_mac_encrypt_hook")
done
if [[ $_omarchy_mac_encrypt_have_ours == false && $_omarchy_mac_encrypt_added_ours == false ]]; then
_omarchy_mac_encrypt_hooks+=(omarchy-mac-encrypt)
fi
if [[ $_omarchy_mac_encrypt_have_sd == false && $_omarchy_mac_encrypt_added_sd == false ]]; then
_omarchy_mac_encrypt_hooks+=(sd-encrypt)
fi
HOOKS=("${_omarchy_mac_encrypt_hooks[@]}")
fi
unset _omarchy_mac_encrypt_hooks _omarchy_mac_encrypt_hook \
_omarchy_mac_encrypt_have_ours _omarchy_mac_encrypt_have_sd \
_omarchy_mac_encrypt_added_ours _omarchy_mac_encrypt_added_sd \
_omarchy_mac_encrypt_have_systemd _omarchy_mac_encrypt_have_vconsole \
_omarchy_mac_encrypt_have_encrypt
@@ -0,0 +1,31 @@
# Origin: omarchy-mx-mac install/hardware/apple/fix-asahi-hid-race.sh
# Apple Silicon internal keyboard and trackpad at the sd-encrypt passphrase
# prompt. dockchannel-hid creates the HID devices; hid_apple binds the
# keyboard and hid_magicmouse the SPI trackpad. MTP machines (M2 Air, M2 Max
# j416c) use dockchannel-hid — linux-aurora has no apple-mtp module. SPI HID
# (M1 Air, M1 Pro j314s) is spi-hid-apple-of, built-in on aurora. usbhid is
# an external USB keyboard at the prompt. thunderbolt (CONFIG_USB4=m) and
# thunderbolt_apple, the Apple Silicon USB4 host router (CONFIG_USB4_APPLE_SOC=m
# on linux-aurora), bring up the USB4/Thunderbolt tunnels, so a keyboard behind
# a USB-C or Thunderbolt dock types at the prompt too (omarchy-mx-mac#86).
#
# mkinitcpio fails the whole image over a MODULES entry it cannot find, or a
# built-in it reports as "(builtin)". Each name is added only when modinfo
# returns a real path. Ending on unset keeps the exit status zero.
for _omarchy_mac_hid_module in \
hid_apple hid_magicmouse dockchannel-hid usbhid \
spi-apple spi-hid-apple spi-hid-apple-of \
apple-dockchannel apple-rtkit-helper thunderbolt thunderbolt_apple; do
_omarchy_mac_hid_path=$(modinfo -k "${KERNELVERSION:-$(uname -r)}" -F filename \
"$_omarchy_mac_hid_module" 2>/dev/null) || continue
[[ $_omarchy_mac_hid_path == /* ]] || continue
MODULES+=("$_omarchy_mac_hid_module")
done
unset _omarchy_mac_hid_module _omarchy_mac_hid_path
# MTP trackpad firmware (apple/tpmtfw-<board>.bin) is not a MODULES entry.
# omarchy-vendorfw-initrd.service unpacks ESP vendorfw/firmware.cpio onto
# /lib/firmware/vendor (symlink to /vendorfw) before cryptsetup-pre.target.
# Do not FILES+= under /lib/firmware/vendor, and do not pre-create
# /vendorfw/apple in the image: that skipped ESP extraction.
@@ -0,0 +1,18 @@
# Plymouth draws the disk password prompt and the boot splash, as on x86
# Omarchy. It goes right after systemd so its initrd units order correctly,
# only when the hook is installed, and never twice.
if [[ -f /usr/lib/initcpio/install/plymouth && " ${HOOKS[*]} " != *" plymouth "* ]]; then
_omarchy_mac_plymouth_hooks=()
_omarchy_mac_plymouth_added=false
for _omarchy_mac_plymouth_hook in "${HOOKS[@]}"; do
_omarchy_mac_plymouth_hooks+=("$_omarchy_mac_plymouth_hook")
if [[ $_omarchy_mac_plymouth_hook == systemd && $_omarchy_mac_plymouth_added == false ]]; then
_omarchy_mac_plymouth_hooks+=(plymouth)
_omarchy_mac_plymouth_added=true
fi
done
if [[ $_omarchy_mac_plymouth_added == true ]]; then
HOOKS=("${_omarchy_mac_plymouth_hooks[@]}")
fi
unset _omarchy_mac_plymouth_hooks _omarchy_mac_plymouth_added _omarchy_mac_plymouth_hook
fi
@@ -0,0 +1,46 @@
# The disk passphrase prompt types with the owner's keyboard layout, as on
# x86 Omarchy: sd-vconsole loads KEYMAP on the console (systemd-ask-password)
# and /etc/vconsole.conf gives Plymouth its XKBLAYOUT. The aarch64
# omarchy-settings drops upstream's omarchy_hooks.conf, so this drop-in
# carries its guard: a layout that does not type Latin letters stays out of
# the initramfs, because a Latin passphrase would be untypeable in it
# (upstream #6229). The prompt then uses the kernel's US map, which is what
# such a passphrase was typed with.
#
# A systemd HOOKS line gets sd-vconsole exactly once, after keyboard (or
# after systemd without one); keymap and consolefont are its busybox
# counterparts and never belong on such a line. A busybox line (a Mac
# unlocked by cryptdevice=, which 91 leaves alone) keeps its hooks; it only
# gets the file for Plymouth, as upstream does.
# No vconsole.conf is the kernel's US map: sd-vconsole stays, nothing to bundle.
_omarchy_mac_vconsole_latin=true
if [[ -f /etc/vconsole.conf ]]; then
_omarchy_mac_vconsole_layout=$(unset XKBLAYOUT; . /etc/vconsole.conf 2>/dev/null; printf '%s' "${XKBLAYOUT:-}")
case ${_omarchy_mac_vconsole_layout%%,*} in
af | am | ara | bd | bg | by | et | ge | gr | il | in | iq | ir | kg | kh | kz | la | lk | mk | mm | mn | mv | np | rs | ru | sy | th | tj | ua)
_omarchy_mac_vconsole_latin=false ;;
esac
fi
if [[ " ${HOOKS[*]} " == *" systemd "* ]]; then
_omarchy_mac_vconsole_hooks=()
_omarchy_mac_vconsole_anchor=systemd
[[ " ${HOOKS[*]} " != *" keyboard "* ]] || _omarchy_mac_vconsole_anchor=keyboard
for _omarchy_mac_vconsole_hook in "${HOOKS[@]}"; do
case $_omarchy_mac_vconsole_hook in
sd-vconsole | keymap | consolefont) continue ;;
esac
_omarchy_mac_vconsole_hooks+=("$_omarchy_mac_vconsole_hook")
if [[ $_omarchy_mac_vconsole_hook == "$_omarchy_mac_vconsole_anchor" && $_omarchy_mac_vconsole_latin == true ]]; then
_omarchy_mac_vconsole_hooks+=(sd-vconsole)
_omarchy_mac_vconsole_anchor=
fi
done
HOOKS=("${_omarchy_mac_vconsole_hooks[@]}")
fi
if [[ $_omarchy_mac_vconsole_latin == true && -f /etc/vconsole.conf ]]; then
FILES+=(/etc/vconsole.conf)
fi
unset _omarchy_mac_vconsole_latin _omarchy_mac_vconsole_layout _omarchy_mac_vconsole_hooks \
_omarchy_mac_vconsole_anchor _omarchy_mac_vconsole_hook
@@ -0,0 +1,84 @@
import QtQuick
import Quickshell
// Drives the real remote-session service against a stub gliff-server on PATH:
// idle at start, active with the ssh peer once a server runs, idle again after
// it exits. The service is never refreshed by hand: a one-shot grim capture
// raises the same Hyprland screencast event that gliff-server does, so the
// event path is what gets exercised.
ShellRoot {
id: root
property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT")
property string stubPidFile: Quickshell.env("OMARCHY_QML_TEST_STUB_PID")
property var failures: []
property var service: null
function fail(message) {
failures.push(String(message))
}
function assertTrue(condition, message) {
if (!condition) fail(message)
}
function shellQuote(value) {
return "'" + String(value).replace(/'/g, "'\\''") + "'"
}
function writeResult() {
var payload = JSON.stringify({ ok: failures.length === 0, failures: failures })
Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)])
}
function captureFrame() {
Quickshell.execDetached(["grim", "-g", "0,0 1x1", Quickshell.env("OMARCHY_QML_TEST_FRAME")])
}
// Polls `condition` every 100 ms for up to `timeout` ms, then continues with
// `next` either way, recording `message` as a failure on timeout. `tick`,
// when given, runs every 500 ms while waiting.
function waitFor(condition, timeout, message, next, tick) {
var timer = Qt.createQmlObject("import QtQuick; Timer { repeat: true; interval: 100 }", root)
var waited = 0
timer.triggered.connect(function() {
waited += timer.interval
if (tick && waited % 500 === 0) tick()
if (!condition() && waited < timeout) return
timer.stop()
timer.destroy()
root.assertTrue(condition(), message)
next()
})
timer.start()
}
Component.onCompleted: {
var component = Qt.createComponent("file://" + Quickshell.env("OMARCHY_PATH") + "/shell/plugins/services/remote-session/Service.qml")
if (component.status !== Component.Ready) {
fail("remote session service failed to load: " + component.errorString())
writeResult()
return
}
service = component.createObject(root, { shell: null })
if (!service) {
fail("remote session service failed to instantiate: " + component.errorString())
writeResult()
return
}
waitFor(function() { return service.stateLoaded === true }, 3000, "service probes on startup", function() {
root.assertTrue(service.active === false, "service starts idle without a gliff server")
Quickshell.execDetached(["bash", "-c", "SSH_CONNECTION='10.0.0.5 51234 10.0.0.1 22' gliff-server --stdio & echo $! > " + root.shellQuote(root.stubPidFile)])
// Each capture raises a screencast event; the first one after the stub
// is up is what should flip the service, with no refresh() by hand.
waitFor(function() { return service.active === true }, 4000, "a screencast event makes the service report the session while gliff-server runs", function() {
root.assertTrue(JSON.stringify(service.peers) === JSON.stringify(["10.0.0.5"]), "service reports the ssh peer, got " + JSON.stringify(service.peers))
Quickshell.execDetached(["bash", "-c", "kill \"$(cat " + root.shellQuote(root.stubPidFile) + ")\""])
waitFor(function() { return service.active === false }, 5000, "service returns to idle once gliff-server exits", function() {
root.writeResult()
})
}, root.captureFrame)
})
}
}
+13 -2
View File
@@ -32,6 +32,17 @@ PY
copy_boundary_file bin/omarchy-security-functions
copy_boundary_file bin/omarchy-update-pacman
copy_boundary_file default/omarchy/sudo-no-update/sudo
copy_boundary_file install/helpers/pacman.sh
# Channel changes copy the platform's templates; the copy itself is a stand-in.
cp -r "$ROOT/default/pacman" "$SUDO_TEST_ROOT/default/"
# Channel changes ask for the platform. x86 unless a test says otherwise, so no
# test reads the host's hardware.
cat >"$SUDO_TEST_ROOT/bin/omarchy-hw-platform" <<'STUB'
#!/bin/bash
echo "${SUDO_TEST_PLATFORM:-x86}"
STUB
chmod +x "$SUDO_TEST_ROOT/bin/omarchy-hw-platform"
cat >"$SUDO_TEST_ROOT/mock/sudo" <<'STUB'
#!/bin/bash
@@ -132,7 +143,7 @@ case "$step" in
esac
STUB
chmod +x "$SUDO_TEST_ROOT/bin/test-step"
for step in omarchy-update-lock omarchy-update-requires-free-space omarchy-update-confirm omarchy-update-pkg-prune omarchy-snapshot omarchy-update-stay-awake omarchy-update-dev omarchy-update-keyring omarchy-update-system-pkgs omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-mise omarchy-update-orphan-pkgs omarchy-update-analyze-logs omarchy-update-status omarchy-update-restart omarchy-pkg-aur-accessible omarchy-notification-dismiss pacman systemd-run cp yay; do
for step in omarchy-update-lock omarchy-update-requires-free-space omarchy-update-confirm omarchy-update-pkg-prune omarchy-snapshot omarchy-update-stay-awake omarchy-update-dev omarchy-update-boot omarchy-update-keyring omarchy-update-system-pkgs omarchy-migrate omarchy-hook omarchy-update-aur-pkgs omarchy-update-mise omarchy-update-orphan-pkgs omarchy-update-analyze-logs omarchy-update-status omarchy-update-restart omarchy-pkg-aur-accessible omarchy-notification-dismiss pacman systemd-run cp yay; do
ln -s test-step "$SUDO_TEST_ROOT/bin/$step"
done
ln -s ../bin/test-step "$SUDO_TEST_ROOT/mock/pacman"
@@ -140,7 +151,7 @@ ln -s ../bin/test-step "$SUDO_TEST_ROOT/mock/pacman"
reset_boundary() {
: >"$SUDO_TEST_LOG"
/usr/bin/rm -f "$SUDO_TEST_CACHE"
unset SUDO_TEST_FAIL_STEP SUDO_TEST_SIGNAL_STEP SUDO_TEST_SUDO_FAIL SUDO_TEST_REVOKE_FAIL SUDO_TEST_UNSUPPORTED SUDO_TEST_REMOVE_WRAPPER_STEP
unset SUDO_TEST_FAIL_STEP SUDO_TEST_SIGNAL_STEP SUDO_TEST_SUDO_FAIL SUDO_TEST_REVOKE_FAIL SUDO_TEST_UNSUPPORTED SUDO_TEST_REMOVE_WRAPPER_STEP SUDO_TEST_PLATFORM
}
assert_boundary_cold() {
[[ ! -e $SUDO_TEST_CACHE ]] || fail "$1 left cached authorization"
@@ -0,0 +1,87 @@
#!/bin/bash
# Runs against the private real boltd created by bolt-integration.py. No policy,
# busctl, firmware reads, or writes are mocked here.
set -euo pipefail
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
TB_STATE="$TB_TEST_DIR/policy.json"
TB_RUNTIME="$TB_TEST_DIR/runtime"
mkdir -p "$TB_RUNTIME"
check() { jq -e "$1" "$2" >/dev/null; }
reject() { if "$@"; then echo 'Unexpected approval success' >&2; exit 1; fi; }
case "$1" in
approve)
echo '{"version":1,"enabled":true,"trusted":{}}' | tb_json_write "$TB_STATE"
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
tb_snapshot > "$TB_TEST_DIR/before.json"
capture=$(tb_capture)
tb_inventory | jq -e --arg uid "$TB_TEST_UID" --argjson capture "$capture" \
'.devices[] | select(.Uid==$uid) | $capture[$uid]=={Uid,Name,Vendor}' >/dev/null
check 'all(.devices[]; .status=="connected" and (.stored|not))' "$TB_TEST_DIR/before.json"
id=$(jq -c --arg uid "$TB_TEST_UID" '.devices[] | select(.identity.Uid==$uid) | .identity' "$TB_TEST_DIR/before.json")
printf '%s\n' "$id" > "$TB_TEST_DIR/identity.json"
tb_approve "$id" true true > "$TB_TEST_DIR/after.json"
jq -e --arg uid "$TB_TEST_UID" '.devices[] | select(.identity.Uid==$uid) | .trusted and .status=="authorized" and .policy=="manual"' "$TB_TEST_DIR/after.json" >/dev/null
jq -e --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .status=="connected"' "$TB_TEST_DIR/after.json" >/dev/null
check 'all(.domains[0].BootACL[]; .=="")' "$TB_TEST_DIR/after.json"
inventory=$(tb_inventory)
if [[ $TB_TEST_SECURITY == "secure" ]]; then
jq -e --arg uid "$TB_TEST_UID" '.devices[] | select(.Uid==$uid) | .Stored and .Key=="have"' <<< "$inventory" >/dev/null
[[ -s $TB_TEST_KEYS/$TB_TEST_UID ]]
sha256sum "$TB_TEST_KEYS/$TB_TEST_UID" > "$TB_TEST_DIR/key-checksum"
fi
owner=$(jq -r .owner <<< "$inventory")
device=$(jq -c --arg uid "$TB_TEST_UID" '.stored[] | select(.Uid==$uid)' <<< "$inventory")
domain=$(jq -c '.domains[0]' <<< "$inventory")
device_path=$(jq -r .path <<< "$device"); domain_path=$(jq -r .path <<< "$domain")
tb_set "$owner" "$device_path" "$TB_DEVICE" Policy '"auto"'
acl=$(jq -c --arg uid "$TB_TEST_UID" '.BootACL | map("") | .[0]=$uid' <<< "$domain")
tb_set "$owner" "$domain_path" "$TB_DOMAIN" BootACL "$acl" as
tb_boot_change true
check .boot_protection "$TB_STATE"
tb_inventory > "$TB_TEST_DIR/inventory.json"
check 'all(.domains[0].BootACL[]; .=="") and .stored[0].Policy=="manual"' "$TB_TEST_DIR/inventory.json"
tb_boot_change false
check '.boot_protection==false' "$TB_STATE"
tb_inventory > "$TB_TEST_DIR/inventory.json"
jq -e --argjson acl "$acl" '.domains[0].BootACL==$acl and .stored[0].Policy=="auto"' "$TB_TEST_DIR/inventory.json" >/dev/null
jq --arg other "$TB_TEST_OTHER" 'del(.trusted[$other])' "$TB_STATE" | tb_json_write "$TB_STATE"
tb_set "$owner" "$device_path" "$TB_DEVICE" Policy '"manual"'
tb_set "$owner" "$domain_path" "$TB_DOMAIN" BootACL "$(jq -c 'map("")' <<< "$acl")" as
;;
restart)
cat /proc/sys/kernel/random/uuid > "$TB_RUNTIME/generation"
reject tb_approve "$(cat "$TB_TEST_DIR/identity.json")" true
tb_reconcile > "$TB_TEST_DIR/after.json"
jq -e --arg uid "$TB_TEST_UID" '.devices[] | select(.identity.Uid==$uid) | .trusted and .status=="authorized"' "$TB_TEST_DIR/after.json" >/dev/null
jq -e --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .status=="connected"' "$TB_TEST_DIR/after.json" >/dev/null
tb_inventory | jq -e '.manager.AuthMode=="disabled"' >/dev/null
if [[ $TB_TEST_SECURITY == "secure" ]]; then
# Bolt must challenge with the originally saved key, not provision a new
# key merely because a reconnecting device advertises the same identity.
tb_inventory | jq -e --arg uid "$TB_TEST_UID" \
'.devices[] | select(.Uid==$uid) | .Key=="have" and (.AuthFlags | contains("secure"))' >/dev/null
sha256sum --status -c "$TB_TEST_DIR/key-checksum"
fi
;;
daemon)
TB_MARKER="$TB_TEST_DIR/enabled" TB_LOCK="$TB_TEST_DIR/root.lock"
touch "$TB_MARKER"
tb_daemon
;;
status)
snapshot=$(tb_public_snapshot)
jq -e --arg owner "$(tb_owner)" '.owner==$owner' <<< "$snapshot" >/dev/null
printf '%s\n' "$snapshot"
;;
once)
snapshot=$(tb_public_snapshot)
id=$(jq -c --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .identity' <<< "$snapshot")
exec 9> "$TB_TEST_DIR/root.lock"
flock -x 9
tb_approve "$id" false > "$TB_TEST_DIR/once.json"
jq -e --arg uid "$TB_TEST_OTHER" '.devices[] | select(.identity.Uid==$uid) | .status=="authorized" and (.trusted|not)' "$TB_TEST_DIR/once.json" >/dev/null
;;
*) exit 64 ;;
esac
@@ -0,0 +1,185 @@
"""Only the hardware fixture is Python: upstream Bolt uses UMockdev/GObject.
All Omarchy behavior and assertions run in bolt-exercise.sh against real boltd,
real busctl, a private system bus, and disposable sysfs/firmware/store state.
"""
import importlib.machinery
import importlib.util
import os
from pathlib import Path
import subprocess
import sys
import time
import unittest
root = Path(sys.argv.pop(1))
loader = importlib.machinery.SourceFileLoader('bolt_upstream', str(Path(os.environ['BOLT_TEST_SOURCE']) / 'tests/test-integration'))
spec = importlib.util.spec_from_loader(loader.name, loader)
u = importlib.util.module_from_spec(spec)
loader.exec_module(u)
class ApprovalIntegration(u.BoltTest):
def exercise(self, security, numeric=False):
self.user_config(AuthMode='disabled', DefaultPolicy='manual')
_, host = self.add_domain_host(security=security, iommu='1')
path, uid = self.add_device(host, 1, 'Dock', 'Example', authorized=0, key='' if security == 'secure' else None, boot='0')
_, other = self.add_device(host, 2, 'Unknown', 'Example', authorized=0, key=None, boot='0')
if numeric:
# Linux omits these files when the peripheral has no descriptive names.
for attribute in ('device_name', 'vendor_name'):
Path(self.testbed.get_root_dir(), path.lstrip('/'), attribute).unlink()
self.daemon_start()
self.polkitd_start()
self.polkitd.SetAllowed(['org.freedesktop.bolt.authorize', 'org.freedesktop.bolt.enroll', 'org.freedesktop.bolt.manage'])
env = os.environ | {'ROOT': str(root), 'UMOCKDEV_DIR': self.testbed.get_root_dir(),
'TB_TEST_DIR': self.rundir, 'TB_TEST_UID': uid, 'TB_TEST_OTHER': other,
'TB_TEST_KEYS': str(Path(self.dbpath, 'keys')), 'TB_TEST_SECURITY': security}
command = ['bash', str(root / 'test/shell.d/fixtures/thunderbolt/bolt-exercise.sh')]
subprocess.run(command + ['approve'], env=env, check=True)
self.daemon_stop()
self.testbed.set_attribute(path, 'authorized', '0')
if security == 'secure':
self.testbed.set_attribute(path, 'key', '\n')
self.daemon_start()
subprocess.run(command + ['restart'], env=env, check=True)
# The real Bash daemon publishes a fresh readable snapshot and reconnects
# to Bolt after a restart. No custom Omarchy D-Bus server is involved.
child = subprocess.Popen(command + ['daemon'], env=env)
try:
for _ in range(100):
result = subprocess.run(command + ['status'], env=env, capture_output=True)
if result.returncode == 0:
break
time.sleep(0.1)
else:
raise RuntimeError('Bash daemon did not publish a valid snapshot')
subprocess.run(command + ['once'], env=env, check=True)
self.daemon_stop()
self.daemon_start()
for _ in range(100):
if subprocess.run(command + ['status'], env=env, capture_output=True).returncode == 0:
break
time.sleep(0.1)
else:
raise RuntimeError('Bash daemon did not publish the new Bolt generation')
finally:
child.terminate()
child.wait(timeout=15)
self.daemon_stop()
def test_user_approval(self):
self.exercise('user')
def test_secure_approval(self):
self.exercise('secure')
def test_numeric_identity(self):
self.exercise('user', numeric=True)
def removal(self, security, boot=False, disconnected=False):
self.user_config(AuthMode='disabled')
_, host = self.add_domain_host(security=security, iommu='1')
path, uid = self.add_device(host, 1, 'Dock', 'Example', authorized=0, key='' if security == 'secure' else None, boot='0')
other_path, other = self.add_device(host, 2, 'Existing', 'Example', authorized=0, key=None, boot='0')
self.daemon_start()
self.polkitd_start()
self.polkitd.SetAllowed(['org.freedesktop.bolt.authorize', 'org.freedesktop.bolt.enroll', 'org.freedesktop.bolt.manage'])
env = os.environ | {'ROOT': str(root), 'UMOCKDEV_DIR': self.testbed.get_root_dir(),
'TB_TEST_DIR': self.rundir, 'TB_TEST_UID': uid, 'TB_TEST_OTHER': other,
'TB_TEST_STORE': self.dbpath, 'TB_TEST_SECURITY': security}
command = ['bash', str(root / 'test/shell.d/fixtures/thunderbolt/bolt-removal.sh')]
service = 'omarchy-thunderbolt-authorization.service'
child = None
def operation(action, unit):
nonlocal child
if unit == 'bolt.service':
if action == 'stop':
self.daemon_stop()
elif self.daemon is None:
self.daemon_start()
elif unit == service:
if action == 'stop' and child is not None:
child.terminate()
child.wait(timeout=15)
child = None
elif action == 'start' and child is None:
child = subprocess.Popen(['bash', str(root / 'test/shell.d/fixtures/thunderbolt/bolt-exercise.sh'), 'daemon'], env=env)
else:
raise RuntimeError('Unexpected private service: ' + unit)
marker = Path(self.rundir, unit + '.active')
if action == 'start':
marker.touch()
else:
marker.unlink(missing_ok=True)
def run(action):
process = subprocess.Popen(command + [action], env=env)
control = Path(self.rundir, 'control')
deadline = time.monotonic() + 90
try:
while process.poll() is None:
if time.monotonic() > deadline:
raise RuntimeError('Private setup timed out: ' + action)
if control.exists():
operation(*control.read_text().strip().split())
control.unlink()
time.sleep(0.05)
if process.returncode:
raise subprocess.CalledProcessError(process.returncode, command + [action])
finally:
if process.poll() is None:
process.terminate()
process.wait(timeout=15)
def wait_for(action):
for _ in range(50):
result = subprocess.run(command + [action], env=env, capture_output=True, text=True)
if result.returncode == 0:
print(result.stdout, end='', flush=True)
return
time.sleep(0.1)
raise RuntimeError(action + ': ' + result.stdout + result.stderr)
def unplug():
for device_path in (path, other_path):
self.testbed.uevent(device_path, 'remove')
self.testbed.remove_device(device_path)
wait_for('disconnected')
try:
Path(self.rundir, 'bolt.service.active').touch()
run('approve')
operation('start', service)
Path(self.rundir, service + '.enabled').touch()
if boot:
run('boot-enable')
run('rollback')
if disconnected:
unplug()
run('disable')
if not disconnected:
unplug()
for number, device_uid, name in ((1, uid, 'Dock'), (2, other, 'Existing')):
attributes = ['device_name', name, 'device', '0x23', 'vendor_name', 'Example',
'vendor', '0x23', 'authorized', '0', 'unique_id', device_uid, 'boot', '0']
if number == 1 and security == 'secure':
attributes += ['key', '\n']
self.testbed.add_device('thunderbolt', '0-' + str(number), host, attributes, ['DEVTYPE', 'thunderbolt_device'])
wait_for('reconnected')
finally:
operation('stop', service)
self.daemon_stop()
def test_user_removal(self):
self.removal('user')
def test_secure_removal(self):
self.removal('secure', boot=True, disconnected=True)
names = sys.argv[1:] or ('test_user_approval', 'test_secure_approval', 'test_numeric_identity', 'test_user_removal', 'test_secure_removal')
suite = unittest.TestSuite(ApprovalIntegration(name) for name in names)
result = unittest.TextTestRunner(verbosity=2).run(suite)
sys.exit(not result.wasSuccessful())
@@ -0,0 +1,95 @@
#!/bin/bash
# Real Bolt policy/approval/setup calls. Only systemd is bridged to the private
# daemon processes owned by bolt-integration.py; never contact host systemd.
set -euo pipefail
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
TB_STATE="$TB_TEST_DIR/policy.json" TB_CONFIG="$TB_TEST_STORE/boltd.conf"
TB_MARKER="$TB_TEST_DIR/enabled" TB_PENDING="$TB_TEST_DIR/pending"
TB_RUNTIME="$TB_TEST_DIR/runtime" TB_LOCK="$TB_TEST_DIR/root.lock"
mkdir -p "$TB_RUNTIME"
service_request() {
printf '%s\n' "$*" > "$TB_TEST_DIR/control.tmp"
mv "$TB_TEST_DIR/control.tmp" "$TB_TEST_DIR/control"
for (( attempt=0; attempt<300; attempt++ )); do
[[ -f $TB_TEST_DIR/control ]] || return 0
sleep 0.05
done
echo "Private service operation timed out: $*" >&2
return 1
}
systemctl() {
local action=$1 unit
shift
printf '%s\n' "$action $*" >> "$TB_TEST_DIR/services.log"
case "$action" in
show)
unit=${!#}
if [[ -f $TB_TEST_DIR/$unit.active ]]; then echo ActiveState=active; else echo ActiveState=inactive; fi
if [[ -f $TB_TEST_DIR/$unit.enabled ]]; then echo UnitFileState=enabled; else echo UnitFileState=disabled; fi ;;
start|stop)
for unit in "$@"; do service_request "$action" "$unit" || return 1; done ;;
enable)
unit=${!#}
touch "$TB_TEST_DIR/$unit.enabled"
if [[ $1 == "--now" ]]; then service_request start "$unit"; fi ;;
disable)
if [[ -f $TB_TEST_DIR/fail-disable ]]; then rm "$TB_TEST_DIR/fail-disable"; return 1; fi
rm -f -- "$TB_TEST_DIR/$1.enabled" ;;
daemon-reload) ;;
*) echo "Unexpected systemctl action: $action $*" >&2; return 1 ;;
esac
}
check_inventory() { tb_inventory | jq -e --arg uid "$TB_TEST_UID" --arg other "$TB_TEST_OTHER" "$1" >/dev/null; }
case "$1" in
approve)
jq -cn '{version:1,enabled:true,trusted:{},original_authmode:"enabled"}' > "$TB_STATE"
echo removal-test > "$TB_RUNTIME/generation"
touch "$TB_MARKER"
identity=$(tb_snapshot | jq -c --arg uid "$TB_TEST_UID" '.devices[]|select(.identity.Uid==$uid)|.identity')
tb_approve "$identity" true true >/dev/null
# An existing manual record, even explicitly trusted through Omarchy,
# must keep the policy its administrator chose before enrollment.
inventory=$(tb_inventory)
owner=$(jq -r .owner <<< "$inventory")
path=$(jq -r --arg uid "$TB_TEST_OTHER" '.devices[]|select(.Uid==$uid)|.path' <<< "$inventory")
tb_bus call "$owner" "$path" "$TB_DEVICE" Authorize s '' >/dev/null
tb_bus call "$owner" "$TB_PATH" "$TB_MANAGER" EnrollDevice sss "$TB_TEST_OTHER" manual '' >/dev/null
identity=$(tb_snapshot | jq -c --arg uid "$TB_TEST_OTHER" '.devices[]|select(.identity.Uid==$uid)|.identity')
tb_approve "$identity" true true >/dev/null
jq -e --arg uid "$TB_TEST_UID" --arg other "$TB_TEST_OTHER" \
'.enrolled[$uid].StoreTime>0 and .enrolled[$other]==null and .trusted[$other]!=null' "$TB_STATE" >/dev/null
if [[ $TB_TEST_SECURITY == "secure" ]]; then
sha256sum "$TB_TEST_STORE/keys/$TB_TEST_UID" > "$TB_TEST_DIR/key-checksum"
fi ;;
boot-enable) tb_admin boot-enable ;;
rollback)
cp "$TB_STATE" "$TB_TEST_DIR/old-policy.json"
tb_inventory | tb_boot_saved > "$TB_TEST_DIR/old-firmware.json"
touch "$TB_TEST_DIR/fail-disable"
if tb_admin disable; then echo 'Failed removal unexpectedly succeeded' >&2; exit 1; fi
cmp "$TB_STATE" "$TB_TEST_DIR/old-policy.json"
cmp <(tb_inventory | tb_boot_saved | jq -cS .) <(jq -cS . "$TB_TEST_DIR/old-firmware.json")
[[ -f $TB_MARKER && -f $TB_TEST_DIR/$TB_SERVICE.active && -f $TB_TEST_DIR/$TB_SERVICE.enabled && ! -f $TB_TEST_DIR/setup-recovery.json ]]
check_inventory '.manager.AuthMode=="disabled"'
echo 'Removal failure restored policy, firmware ACLs and approval service' ;;
disable)
tb_admin disable
[[ ! -f $TB_MARKER && ! -f $TB_TEST_DIR/$TB_SERVICE.active && ! -f $TB_TEST_DIR/$TB_SERVICE.enabled ]]
jq -e '.enabled==false and .enrolled==null' "$TB_STATE" >/dev/null
check_inventory '.manager.AuthMode=="enabled" and any(.stored[]; .Uid==$uid and .Policy=="auto") and any(.stored[]; .Uid==$other and .Policy=="manual")'
if [[ $TB_TEST_SECURITY == "secure" ]]; then sha256sum --status -c "$TB_TEST_DIR/key-checksum"; fi
echo 'Removal handed our record back to Bolt and preserved existing manual policy' ;;
disconnected)
check_inventory 'any(.stored[]; .Uid==$uid and .Status=="disconnected") and any(.stored[]; .Uid==$other and .Status=="disconnected")' ;;
reconnected)
check_inventory 'any(.devices[]; .Uid==$uid and .Policy=="auto" and (.Status|startswith("authorized"))) and any(.devices[]; .Uid==$other and .Policy=="manual" and .Status=="connected")'
if [[ $TB_TEST_SECURITY == "secure" ]]; then
check_inventory 'any(.devices[]; .Uid==$uid and .Key=="have" and (.AuthFlags|contains("secure")))'
sha256sum --status -c "$TB_TEST_DIR/key-checksum"
fi
echo "Automatic reconnect succeeded in $TB_TEST_SECURITY mode; pre-existing manual device stayed blocked" ;;
*) exit 64 ;;
esac
@@ -0,0 +1,640 @@
#!/bin/bash
# These behavior cases were first run as the existing Python assertions against
# the Bash implementation. Fixtures replace only Bolt, systemd and desktop I/O.
set -euo pipefail
source "$ROOT/test/shell.d/base-test.sh"
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
source "$ROOT/install/helpers/thunderbolt-review.sh"
fixture() {
T=$(mktemp -d)
trap 'rm -rf -- "$T"' EXIT
export HOME="$T/home"
TB_STATE="$T/policy.json" TB_CONFIG="$T/boltd.conf" TB_MARKER="$T/enabled"
TB_PENDING="$T/pending" TB_SYSFS="$T/sysfs" TB_KEYS="$T/keys"
TB_RUNTIME="$T/runtime" TB_LOCK="$T/root.lock"
mkdir -p "$TB_RUNTIME" "$HOME" "$TB_SYSFS" "$TB_KEYS"
echo generation-a > "$TB_RUNTIME/generation"
echo '{"version":1,"enabled":true,"trusted":{},"original_authmode":"enabled"}' > "$TB_STATE"
cat > "$T/inventory" <<'JSON'
{"owner":":1.5","manager":{"AuthMode":"disabled"},"domains":[{"Uid":"host","SecurityLevel":"user","IOMMU":true,"SysfsPath":"/sys/domain","path":"/domain","BootACL":["device-1",""]}],"devices":[{"Uid":"device-1","Name":"Dock","Vendor":"Vendor","Parent":"host","SysfsPath":"/sys/devices/test/0-1","ConnectTime":1,"Status":"connected","Stored":false,"Policy":"default","AuthFlags":"","path":"/devices/1","inode":"1"}]}
JSON
: > "$T/operations"; : > "$T/units"; : > "$T/notifications"
echo '{}' > "$T/capture"
tb_requests
}
edit_inventory() { jq "$@" "$T/inventory" > "$T/next"; mv "$T/next" "$T/inventory"; }
edit_state() { jq "$@" "$TB_STATE" | tb_json_write "$TB_STATE"; }
check() { jq -e "$1" "$2" >/dev/null || fail "$1" "$(cat "$2")"; }
reject() { if ("$@") > "$T/output" 2> "$T/error"; then fail "unexpected success: $*"; fi; }
identity() { tb_snapshot | jq -c '.devices[0].identity'; }
tb_inventory() {
[[ ! -f $T/inventory-failure ]] || { tb_fail 'inventory unavailable'; return 1; }
jq '. + {stored:[.devices[] | select(.Stored)]}' "$T/inventory"
}
tb_properties() { jq -c --arg path "$2" '(.devices + .domains)[] | select(.path==$path)' "$T/inventory"; }
tb_bus() {
local command=$1 path=$3 member=$5 value
printf '%s\n' "$member" >> "$T/operations"
case "$member" in
Authorize)
[[ ! -f $T/authorize-failure ]] || return 1
if [[ ! -f $T/authorize-noop ]]; then edit_inventory --arg path "$path" '(.devices[] | select(.path==$path)).Status="authorized"'; fi ;;
EnrollDevice)
if [[ ! -f $T/enroll-noop ]]; then
edit_inventory '(.devices[0] | .Stored=true | .Policy="manual" | .StoreTime=100) as $d | .devices[0]=$d'
if [[ -f $TB_SYSFS/0-1/key && $(cat "$TB_SYSFS/0-1/key") =~ ^[0-9a-f]{64}$ && ! -f $T/key-store-failure ]]; then
edit_inventory '.devices[0].Key="have"'
fi
fi
if [[ -f $T/enroll-lost-reply ]]; then rm "$T/enroll-lost-reply"; return 1; fi ;;
Policy|BootACL)
if [[ $member == "BootACL" && -f $T/boot-failure ]] || [[ $member == "Policy" && ${7:-} == "auto" && -f $T/restore-failure ]]; then
tb_fail 'firmware rejected write'; return 1
fi
[[ $member != "Policy" || ! -f $T/policy-noop ]] || return 0
if [[ $member == "BootACL" ]]; then
shift 7
value=$(jq -cn '$ARGS.positional' --args "$@")
else
value=$(jq -cn --arg value "$7" '$value')
fi
edit_inventory --arg path "$path" --arg key "$member" --argjson value "$value" \
'((.devices[],.domains[]) | select(.path==$path))[$key]=$value'
if [[ $member == "Policy" ]]; then
# Real Bolt changes firmware ACLs when a stored policy changes.
edit_inventory --arg path "$path" --arg value "$7" '
(.devices[] | select(.path==$path) | .Uid) as $uid |
.domains[].BootACL |= (if $value=="auto" and (index($uid)==null) then
(index("") // 0) as $slot | .[$slot]=$uid
elif $value!="auto" then map(if .==$uid then "" else . end) else . end)'
fi ;;
*) tb_fail "Unexpected Bolt call: $*"; return 1 ;;
esac
}
tb_capture() { [[ ! -f $T/capture-failure ]] && cat "$T/capture"; }
tb_unit_state() { jq -cn --arg unit "$1" '{active:($unit=="bolt.service"),enabled:false}'; }
systemctl() {
if [[ $1 == --root=* ]]; then command systemctl "$@"; return; fi
printf '%s\n' "$*" >> "$T/units"
if [[ -f $T/unit-failure && $* == "$(cat "$T/unit-failure")" ]]; then
[[ -f $T/repeat-failure ]] || rm "$T/unit-failure"
tb_fail 'systemd failed'; return 1
fi
if [[ $* == "start bolt.service" && -f $TB_CONFIG ]]; then
edit_inventory --arg mode "$(tb_config_authmode "$TB_CONFIG")" '.manager.AuthMode=$mode'
if [[ ! -f $TB_MARKER && -f $T/restart-policy-failure ]]; then
rm "$T/restart-policy-failure"
edit_inventory '.devices[0].Policy="manual"'
fi
fi
}
tb_public_snapshot() { cat "$T/public"; }
tb_notify() {
echo "$1" >> "$T/notifications"
if [[ -f $T/notify-failure ]]; then rm "$T/notify-failure"; return 1; fi
}
tb_attention() { echo attention >> "$T/notifications"; }
gum() { if [[ $1 == "choose" ]]; then cat "$T/choice"; fi; }
tb_request_apply() {
jq --argjson identity "$2" --argjson permanent "$1" \
'(.devices[] | select(.identity==$identity)) |= (.status="authorized" | .trusted=$permanent | .stored=$permanent)' "$T/public" > "$T/next"
mv "$T/next" "$T/public"
[[ ! -f $T/lost-reply ]] || { tb_fail 'reply lost'; return 1; }
cat "$T/public"
}
request() { local paths=("$TB_REQUESTS"/request-*.json); printf '%s\n' "${paths[0]}"; }
request_count() { local paths; shopt -s nullglob; paths=("$TB_REQUESTS"/request-*.json); echo "${#paths[@]}"; }
public_fixture() { tb_snapshot > "$T/public"; }
boot_fixture() { edit_inventory '.devices[0] |= (.Stored=true | .Policy="auto")'; }
review_tick() {
local path watch
path=$(request); watch=$(jq -r .watch "$path")
tb_scan "$watch" >/dev/null
}
test_iommu_is_not_consent() {
edit_inventory '.devices[0] |= (.Stored=true | .Policy="iommu")'
tb_reconcile > "$T/result"
[[ ! -s $T/operations ]]; check '.devices[0].trusted==false' "$T/result"
}
test_once_is_not_saved() {
tb_approve "$(identity)" false > "$T/result"
check '.warnings==[]' "$T/result"; check '.trusted=={}' "$TB_STATE"
edit_inventory '.devices[0].Status="connected"'
tb_reconcile >/dev/null
[[ $(cat "$T/operations") == "Authorize" ]]
}
test_always_survives_restart() {
tb_approve "$(identity)" true >/dev/null
edit_inventory '.owner=":1.9" | .devices[0] |= (.Status="connected" | .inode="2")'
echo generation-b > "$TB_RUNTIME/generation"
tb_reconcile > "$T/result"
check '.devices[0].status=="authorized" and .devices[0].trusted' "$T/result"
check '.manager.AuthMode=="disabled"' "$T/inventory"
}
test_enrollment_lost_reply_retains_ownership() {
touch "$T/enroll-lost-reply"
reject tb_approve "$(identity)" true
check '.trusted=={} and .enrolled["device-1"].StoreTime==null' "$TB_STATE"
tb_approve "$(identity)" true >/dev/null
check '.enrolled["device-1"].StoreTime==100 and .trusted["device-1"]!=null' "$TB_STATE"
}
secure_fixture() {
mkdir -p "$TB_SYSFS/0-1"
echo 11111111-2222-3333-4444-555555555555 > "$TB_SYSFS/0-1/unique_id"
echo 0 > "$TB_SYSFS/0-1/authorized"
: > "$TB_SYSFS/0-1/key"
edit_inventory --arg path "$TB_SYSFS/0-1" --arg inode "$(stat -Lc %i "$TB_SYSFS/0-1")" \
'.domains[0].SecurityLevel="secure" | .devices[0] |= (.Secure=true | .Key="missing" |
.Uid="11111111-2222-3333-4444-555555555555" | .SysfsPath=$path | .inode=$inode)'
}
test_secure_approval_initializes_before_trust() {
secure_fixture
tb_approve "$(identity)" true true > "$T/result"
[[ $(cat "$TB_SYSFS/0-1/key") =~ ^[0-9a-f]{64}$ ]]
check '.devices[0].trusted and .devices[0].stored' "$T/result"
cp "$TB_SYSFS/0-1/key" "$T/original-key"
tb_approve "$(identity)" true true >/dev/null
cmp "$TB_SYSFS/0-1/key" "$T/original-key"
}
test_secure_automatic_restore_cannot_initialize_keys() {
secure_fixture
jq '.devices[0] | {(.Uid):{Uid,Name,Vendor}}' "$T/inventory" > "$T/trusted"
edit_state --slurpfile trusted "$T/trusted" '.trusted=$trusted[0]'
tb_reconcile > "$T/result"
check '.devices[0].trusted==false' "$T/result"
[[ ! -s $T/operations && ! -s $TB_SYSFS/0-1/key ]]
reject tb_approve "$(identity)" true
[[ ! -s $T/operations ]]
}
test_secure_missing_saved_key_cannot_claim_success() {
secure_fixture
touch "$T/key-store-failure"
reject tb_approve "$(identity)" true true
check '.trusted=={}' "$TB_STATE"
tb_snapshot > "$T/result"; check '.devices[0].trusted==false' "$T/result"
}
test_secure_key_failure_does_not_authorize() {
secure_fixture
openssl() { return 1; }
reject tb_approve "$(identity)" true true
[[ ! -s $T/operations && ! -s $TB_SYSFS/0-1/key ]]
unset -f openssl
echo invalid > "$TB_SYSFS/0-1/key"
reject tb_approve "$(identity)" true true
[[ ! -s $T/operations ]]
}
test_authorized_keyless_device_requires_explicit_reconnect() {
secure_fixture
edit_inventory '.devices[0].Status="authorized"'
echo 1 > "$TB_SYSFS/0-1/authorized"
reject tb_approve "$(identity)" true true
rg -q 'disconnect, reconnect' "$T/error"
[[ ! -s $TB_SYSFS/0-1/key ]]; check '.trusted=={}' "$TB_STATE"
edit_inventory '.devices[0].Status="connected"'
tb_reconcile >/dev/null
[[ $(cat "$T/operations") == "EnrollDevice" ]]
}
test_stale_identity_rejected() {
local id field
id=$(identity)
for field in inode Name ConnectTime; do
cp "$T/inventory" "$T/original"
edit_inventory --arg field "$field" '.devices[0][$field]="replacement"'
reject tb_approve "$id" true
mv "$T/original" "$T/inventory"
done
edit_inventory '.owner=":1.9"'; reject tb_approve "$id" true
[[ ! -s $T/operations ]]
}
test_false_success_rejected() {
local id; id=$(identity)
touch "$T/authorize-noop"; reject tb_approve "$id" true; check '.trusted=={}' "$TB_STATE"
rm "$T/authorize-noop"; touch "$T/enroll-noop"
reject tb_approve "$id" true; check '.trusted=={}' "$TB_STATE"
rm "$T/enroll-noop"; tb_approve "$id" true > "$T/result"
check '.devices[0].trusted' "$T/result"
[[ $(rg -c '^Authorize$' "$T/operations") == 2 ]]
}
test_write_failure_retains_retry() {
local id; id=$(identity)
eval "$(declare -f tb_json_write | sed '1s/tb_json_write/disk_write/')"
tb_json_write() { [[ ! -f $T/disk-full ]] && disk_write "$@"; }
touch "$T/disk-full"; reject tb_approve "$id" true; check '.trusted=={}' "$TB_STATE"
rm "$T/disk-full"; tb_approve "$id" true > "$T/result"
check '.devices[0].trusted' "$T/result"
[[ $(rg -c '^Authorize$' "$T/operations") == 1 ]]
}
test_firmware_bypass_warns() {
edit_inventory '.domains[0].SecurityLevel="none" | .devices[0] |= (.Status="authorized" | .AuthFlags="boot")'
tb_snapshot > "$T/result"; check '.warnings|length==2' "$T/result"; [[ ! -s $T/operations ]]
}
test_global_enable_not_protected() {
edit_inventory '.manager.AuthMode="enabled"'
reject tb_snapshot; rg -q 'not active' "$T/error"
reject tb_reconcile
}
test_display_only_needs_no_approval() {
edit_state '.trusted["device-1"]={Uid:"device-1",Name:"Dock",Vendor:"Vendor"}'
edit_inventory '.devices[0].AuthFlags="nopcie"'
tb_reconcile > "$T/result"; check '.error==""' "$T/result"; [[ ! -s $T/operations ]]
}
test_empty_capture_preserved() {
rm "$TB_STATE"; tb_prepare
touch "$T/capture-failure"; tb_prepare
check '.trusted=={}' "$TB_STATE"; [[ $(tb_config_authmode "$TB_CONFIG") == "disabled" ]]
}
test_capture_failure_does_not_publish() {
rm "$TB_STATE"; touch "$T/capture-failure"; reject tb_prepare
[[ ! -e $TB_STATE && ! -e $TB_MARKER ]]
}
test_capture_optional_names_match_numeric_identity() {
source "$ROOT/install/helpers/thunderbolt-setup.sh"
mkdir -p "$TB_SYSFS/0-1"
echo device-1 > "$TB_SYSFS/0-1/unique_id"
echo 0x1234 > "$TB_SYSFS/0-1/device"
echo 0xab > "$TB_SYSFS/0-1/vendor"
tb_capture > "$T/result"
check '.["device-1"]=={Uid:"device-1",Name:"0x1234",Vendor:"0xab"}' "$T/result"
echo Dock > "$TB_SYSFS/0-1/device_name"
tb_capture > "$T/result"; check '.["device-1"].Name=="Dock" and .["device-1"].Vendor=="0xab"' "$T/result"
rm "$TB_SYSFS/0-1/device_name"; echo Vendor > "$TB_SYSFS/0-1/vendor_name"
tb_capture > "$T/result"; check '.["device-1"].Name=="0x1234" and .["device-1"].Vendor=="Vendor"' "$T/result"
mkdir "$TB_SYSFS/0-1/device_name"
reject tb_capture
}
test_first_activation_defers_without_touching_bolt() {
secure_fixture; rm "$TB_STATE"
printf '[config]\nAuthMode=enabled\n' > "$TB_CONFIG"; cp "$TB_CONFIG" "$T/original"
tb_admin owner
[[ -f $TB_PENDING && ! -f $TB_STATE && ! -f $TB_MARKER && ! -s $T/units ]]
cmp "$TB_CONFIG" "$T/original"
rm -rf "$TB_SYSFS/0-1"
tb_admin migrate
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
tb_admin enable
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_saved_secure_binding_allows_initial_capture() {
secure_fixture; rm "$TB_STATE"
edit_inventory '.devices[0] |= (.Stored=true | .Key="have")'
tb_admin owner
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_key_without_record_defers_initial_capture() {
secure_fixture; rm "$TB_STATE"
edit_inventory '.devices[0].Key="have"'
tb_admin owner
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
}
test_offline_prepare_cannot_trust_installers_bolt() {
secure_fixture; rm "$TB_STATE"
edit_inventory '.devices[0] |= (.Stored=true | .Key="have")'
tb_admin prepare
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
}
test_deferred_opt_out_retains_initial_enrollment() {
secure_fixture; rm "$TB_STATE"
tb_admin owner
tb_admin disable
[[ ! -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
check '.enabled==false and .enrollment_pending==true' "$TB_STATE"
tb_admin migrate
[[ ! -f $TB_PENDING && ! -s $T/units ]]
tb_admin enable
[[ -f $TB_PENDING && ! -f $TB_MARKER && ! -s $T/units ]]
rm -rf "$TB_SYSFS/0-1"
echo '{"new-device":{}}' > "$T/capture"
tb_admin enable
check '.enabled and .enrollment_pending==null and .trusted=={"new-device":{}}' "$TB_STATE"
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_pending_notice_retries_without_claiming_protection() {
touch "$TB_PENDING"
local count=0 notices=0
sleep() { count=$((count + 1)); (( count < 3 )) || exit 99; }
tb_setup_notice() { notices=$((notices + 1)); (( notices > 1 )) && echo pending >> "$T/notifications"; }
reject tb_watch
[[ $(cat "$T/notifications") == "pending" ]]
tb_status > "$T/result"; rg -q 'protection is deferred' "$T/result"
}
test_active_policy_is_not_deferred() {
secure_fixture; touch "$TB_MARKER"
tb_admin owner
[[ ! -f $TB_PENDING && -f $TB_MARKER ]]
}
test_pending_only_factory_reset_reaches_both_roots() {
local function_name root="$T/top/@factory"
local TOP_MNT="$T/top" NEXT_NAME=@next PROVISIONING_DIR=/var/lib/omarchy/provisioning LOG_FILE="$T/reset.log" swap_done=0
mkdir -p "$root/etc/omarchy" "$root/etc/systemd/system" "$root/usr/bin" \
"$root/usr/share/omarchy/install/provisioning" "$T/top/@"
touch "$root/etc/omarchy/thunderbolt-authorization.pending" "$root/usr/bin/omarchy-provision-owner"
chmod +x "$root/usr/bin/omarchy-provision-owner"
echo 'root:x:0:0:root:/root:/bin/bash' > "$root/etc/passwd"
cp "$ROOT/install/provisioning/omarchy-provision-owner.service" \
"$ROOT/install/provisioning/omarchy-system-factory-reset-finish.service" "$root/usr/share/omarchy/install/provisioning/"
cp "$ROOT/etc/systemd/system/omarchy-thunderbolt-authorization.service" "$root/etc/systemd/system/"
for function_name in install_provisioning_units scrub_factory_accounts sanitize_factory_baseline stage_full_reset; do
eval "$(sed -n "/^$function_name() {$/,/^}/p" "$ROOT/bin/omarchy-system-factory-reset")"
done
log() { :; }
btrfs() {
if [[ $1 == "subvolume" && $2 == "snapshot" ]]; then cp -a "$3" "$4"
elif [[ $1 == "property" ]]; then :
else return 1; fi
}
usermod() { [[ $1 == "--root" && $2 == "$TOP_MNT/"* && $3 == "--password" && $4 == "!" && $5 == "root" ]]; }
systemd-id128() { echo new-machine-id; }
encrypted_install() { return 1; }
rebuild_next_boot() { :; }
sync() { :; }
systemctl() {
[[ ( $1 == "--root=$TOP_MNT/@next" || $1 == "--root=$TOP_MNT/@factory" ) && $2 == "disable" && $3 == "$TB_SERVICE" ]] || return 1
command systemctl "$@"
}
function /usr/bin/omarchy-thunderbolt-authorization-admin() {
[[ $1 == "reset-root" ]] || return 1
printf '%s\n' "$2" >> "$T/reset-roots"
(
source "$ROOT/install/helpers/thunderbolt-policy.sh"
source "$ROOT/install/helpers/thunderbolt-setup.sh"
tb_reset_root "$2"
)
}
stage_full_reset
[[ $(wc -l < "$T/reset-roots") == 2 && $swap_done == 1 ]]
[[ ! -f $TOP_MNT/@/etc/omarchy/thunderbolt-authorization.pending &&
! -f $TOP_MNT/@factory/etc/omarchy/thunderbolt-authorization.pending ]]
[[ -f $TOP_MNT/@/var/lib/omarchy/provisioning/pending ]]
}
test_config_preserves_settings() {
rm "$TB_STATE"; printf '[config]\nAuthMode=enabled\nDefaultPolicy=manual\n' > "$TB_CONFIG"
tb_prepare; rg -q '^DefaultPolicy=manual$' "$TB_CONFIG"
check '.original_authmode=="enabled"' "$TB_STATE"
}
test_fresh_owner_replaces_trust() {
rm "$TB_STATE"; echo '{"old":{}}' > "$T/capture"; tb_prepare
echo '{"new":{}}' > "$T/capture"; tb_prepare true
check '.trusted=={"new":{}} and .original_authmode=="enabled"' "$TB_STATE"
}
test_enable_rollback() {
rm "$TB_STATE"; printf '[config]\nAuthMode=enabled\nDefaultPolicy=manual\n' > "$TB_CONFIG"
cp "$TB_CONFIG" "$T/original"; echo 'start bolt.service' > "$T/unit-failure"
reject tb_transaction tb_enable
cmp "$TB_CONFIG" "$T/original"; [[ ! -e $TB_STATE && ! -e $TB_MARKER && ! -e $T/setup-recovery.json ]]
[[ $(tail -2 "$T/units") == $'start bolt.service\nstop omarchy-thunderbolt-authorization.service' ]]
}
test_failed_restore_keeps_checkpoint() {
rm "$TB_STATE"; echo '[config]' > "$TB_CONFIG"; cp "$TB_CONFIG" "$T/original"
echo 'stop omarchy-thunderbolt-authorization.service bolt.service' > "$T/unit-failure"; touch "$T/repeat-failure"
reject tb_transaction tb_enable; rg -q 'restoration is incomplete' "$T/error"
[[ -f $T/setup-recovery.json ]]; reject tb_transaction tb_enable; rg -q 'needs recovery' "$T/error"
echo partial > "$TB_CONFIG"; rm "$T/unit-failure"
tb_config_restore; cmp "$TB_CONFIG" "$T/original"
}
test_disable_restores_matching_boot_state() {
boot_fixture; tb_prepare; tb_boot_change true; cp "$TB_STATE" "$T/original"
echo 'disable omarchy-thunderbolt-authorization.service' > "$T/unit-failure"
reject tb_transaction tb_disable
cmp "$TB_STATE" "$T/original"; [[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
check '.devices[0].Policy=="manual" and .domains[0].BootACL==["",""]' "$T/inventory"
}
test_disable_hands_off_only_our_records() {
tb_prepare
tb_approve "$(identity)" true >/dev/null
edit_inventory '.devices += [(.devices[0] | .Uid="existing" | .path="/devices/2" | .Policy="manual")]'
tb_approve "$(tb_snapshot | jq -c '.devices[1].identity')" true >/dev/null
tb_admin disable
check '.enabled==false and .enrolled==null and (.trusted|length)==2' "$TB_STATE"
check '.manager.AuthMode=="enabled" and .devices[0].Policy=="auto" and .devices[1].Policy=="manual"' "$T/inventory"
[[ ! -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
# After a completed handoff, an administrator may choose manual themselves.
edit_inventory '.devices[0].Policy="manual"'
tb_admin enable
tb_admin disable
check 'all(.devices[]; .Policy=="manual")' "$T/inventory"
}
test_disable_hands_off_disconnected_device() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.devices[0].Status="disconnected"'
tb_admin disable
check '.devices[0].Policy=="auto"' "$T/inventory"
}
test_disable_preserves_recreated_manual_record() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.devices[0].StoreTime=200'
tb_admin disable
check '.devices[0].Policy=="manual"' "$T/inventory"
}
test_disable_preserves_original_disabled_mode() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_state '.original_authmode="disabled"'
tb_admin disable
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="auto"' "$T/inventory"
}
test_disable_handoff_after_boot_protection() {
tb_prepare; tb_boot_change true
tb_approve "$(identity)" true >/dev/null
tb_admin disable
check '.enabled==false and .boot_protection==false' "$TB_STATE"
check '.devices[0].Policy=="auto" and (.domains[0].BootACL|index("device-1"))!=null' "$T/inventory"
}
test_disable_handoff_rolls_back_policies_and_acl() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.domains[0].BootACL=["unrelated",""]'
cp "$TB_STATE" "$T/original"
echo 'disable omarchy-thunderbolt-authorization.service' > "$T/unit-failure"
reject tb_admin disable
cmp "$TB_STATE" "$T/original"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual" and .domains[0].BootACL==["unrelated",""]' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_handoff_failure_keeps_protection() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
tb_unit_state() { echo '{"active":true,"enabled":true}'; }
touch "$T/restore-failure"
reject tb_admin disable
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual"' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
! rg -q '^disable ' "$T/units"
[[ $(tail -1 "$T/units") == "start omarchy-thunderbolt-authorization.service" ]]
}
test_disable_handoff_verifies_policy_writes() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
touch "$T/policy-noop"
reject tb_admin disable
rg -q 'did not retain the requested Policy' "$T/error"
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual"' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_handoff_verifies_policy_after_restart() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
touch "$T/restart-policy-failure"
reject tb_admin disable
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual"' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_handoff_incomplete_rollback_is_recoverable() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
echo 'disable omarchy-thunderbolt-authorization.service' > "$T/unit-failure"
touch "$T/boot-failure"
reject tb_admin disable
rg -q 'restoration is incomplete' "$T/error"
[[ -f $T/setup-recovery.json ]]
rm "$T/boot-failure"
tb_admin recover
check '.enabled and .enrolled["device-1"]!=null' "$TB_STATE"
check '.manager.AuthMode=="disabled" and .devices[0].Policy=="manual" and .domains[0].BootACL==["device-1",""]' "$T/inventory"
[[ -f $TB_MARKER && ! -f $T/setup-recovery.json ]]
}
test_disable_offline_controller_is_unchanged() {
tb_prepare; tb_approve "$(identity)" true >/dev/null
edit_inventory '.domains[0].SysfsPath=""'
reject tb_admin disable
rg -q 'Reconnect every Thunderbolt controller' "$T/error"
check '.enabled' "$TB_STATE"
[[ ! -s $T/units && ! -f $T/setup-recovery.json ]]
}
test_boot_policy_reconnection_and_restore() {
boot_fixture; tb_boot_change true
check '.devices[0].Policy=="manual" and .domains[0].BootACL==["",""]' "$T/inventory"
check '.boot_protection and .trusted["device-1"]!=null' "$TB_STATE"
edit_inventory '.devices[0].Policy="auto" | .domains[0].BootACL=["device-1",""]'
tb_snapshot > "$T/result"; check '.warnings|length>0' "$T/result"
tb_reconcile > "$T/result"; check '.error==""' "$T/result"
check '.domains[0].BootACL==["",""]' "$T/inventory"
tb_boot_change false
check '.devices[0].Policy=="auto" and .domains[0].BootACL==["device-1",""]' "$T/inventory"
}
test_boot_change_rollback() {
boot_fixture; edit_inventory '.domains[0].BootACL=["device-1","other"]'
cp "$TB_STATE" "$T/original"; touch "$T/boot-failure"
reject tb_boot_change true
check '.devices[0].Policy=="auto" and .domains[0].BootACL==["device-1","other"]' "$T/inventory"
cmp <(jq -cS . "$TB_STATE") <(jq -cS . "$T/original"); [[ ! -e $T/boot-recovery.json ]]
}
test_boot_recovery_retained() {
boot_fixture; edit_inventory '.domains[0].BootACL=["device-1","other"]'
cp "$TB_STATE" "$T/original"; touch "$T/boot-failure" "$T/restore-failure"
reject tb_boot_change true; rg -q 'recovery is incomplete' "$T/error"; [[ -f $T/boot-recovery.json ]]
rm "$T/boot-failure" "$T/restore-failure"; tb_boot_recover
cmp <(jq -cS . "$TB_STATE") <(jq -cS . "$T/original"); [[ ! -e $T/boot-recovery.json ]]
}
test_unsupported_boot_cannot_succeed() {
boot_fixture; cp "$T/inventory" "$T/original"
local expression
for expression in '.SysfsPath=""' '.SecurityLevel="none"' '.BootACL=[]'; do
cp "$T/original" "$T/inventory"; edit_inventory ".domains[0] |= ($expression)"
reject tb_boot_change true
done
[[ ! -s $T/operations ]]
}
test_install_defers_owner() {
install() { :; }
function /usr/bin/omarchy-thunderbolt-authorization-admin() { echo "helper $*"; }
export OMARCHY_PATH=$ROOT OMARCHY_INSTALL_USER=''
source "$ROOT/install/config/thunderbolt-authorization.sh" > "$T/output"
! rg -q helper "$T/output"; [[ ! -s $T/units ]]
OMARCHY_INSTALL_USER=lab
source "$ROOT/install/config/thunderbolt-authorization.sh" > "$T/output"
rg -q 'helper prepare' "$T/output"; rg -q '^enable omarchy-thunderbolt-authorization.service$' "$T/units"
}
test_offline_reset_scrubs_private_state() {
source "$ROOT/install/helpers/thunderbolt-policy.sh"
local root="$T/clone" unit="$TB_SERVICE"
mkdir -p "$root/etc/systemd/system" "$root${TB_STATE%/*}" "$root${TB_MARKER%/*}"
cp "$ROOT/etc/systemd/system/$unit" "$root/etc/systemd/system/"
command systemctl --root="$root" enable "$unit" >/dev/null 2>&1
touch "$root$TB_MARKER" "$root$TB_STATE" "$root${TB_STATE%/*}/setup-recovery.json"
local kind
for kind in devices keys domains; do mkdir -p "$root/var/lib/boltd/$kind"; touch "$root/var/lib/boltd/$kind/old"; done
tb_reset_root "$root"
[[ ! -e $root$TB_MARKER && ! -e $root${TB_STATE%/*} && ! -e $root/etc/systemd/system/multi-user.target.wants/$unit ]]
[[ $(ls "$root/var/lib/boltd") == "boltd.conf" && $(tb_config_authmode "$root/var/lib/boltd/boltd.conf") == "enabled" ]]
}
test_live_reset_refused() { reject tb_reset_root /; }
test_trusted_reconnect_no_alert() {
edit_state '.trusted["device-1"]={Uid:"device-1",Name:"Dock",Vendor:"Vendor"}'
public_fixture; tb_scan watch1 >/dev/null; [[ ! -s $T/notifications ]]
}
test_unavailable_warns_once() {
tb_scan() { return 1; }
sleep() { count=$((count+1)); ((count<4)) || exit 99; }
local count=0
reject tb_watch
[[ $(cat "$T/notifications") == "attention" ]]
}
test_failed_delivery_retries_other_devices() {
edit_inventory '.devices += [(.devices[0] | .Uid="device-2")]'; public_fixture; touch "$T/notify-failure"
tb_scan watch1 >/dev/null; [[ $(request_count) == 1 ]]
tb_scan watch1 >/dev/null; [[ $(request_count) == 2 && $(wc -l < "$T/notifications") == 3 ]]
}
test_restarts_restore_requests() {
public_fixture; tb_scan watch1 >/dev/null; local old; old=$(request)
tb_scan watch1 >/dev/null; [[ $(wc -l < "$T/notifications") == 1 ]]
edit_inventory '.owner=":1.9"'; public_fixture
tb_scan watch1 >/dev/null; [[ $(request) != "$old" ]]
tb_scan watch2 >/dev/null; [[ $(request_count) == 1 && $(wc -l < "$T/notifications") == 3 ]]
}
test_inventory_failure_preserves_request() {
public_fixture; tb_scan watch1 >/dev/null; local path; path=$(request)
rm "$T/public"; reject tb_scan watch1; [[ -f $path ]]
}
test_replacement_during_dialog_rejected() {
public_fixture; tb_scan watch1 >/dev/null; local path; path=$(request)
gum() {
if [[ $1 == "choose" ]]; then
jq '.devices[0].identity.Uid="replacement"' "$T/public" > "$T/next"; mv "$T/next" "$T/public"
echo 'Always allow this device'
fi
}
reject tb_review "${path##*/}" # A filename is not a valid token.
reject tb_review "$(basename "$path" .json)"
check '.devices[0].status=="connected"' "$T/public"
}
test_lost_reply_keeps_approval_intent() {
public_fixture; tb_scan watch1 >/dev/null; local path; path=$(request)
echo 'Always allow this device' > "$T/choice"; touch "$T/lost-reply"
sleep() { review_tick; }
reject tb_review "$(basename "$path" .json)"
check '.approval=="Always allow this device"' "$path"
tb_scan watch1 >/dev/null; [[ -f $path && $(wc -l < "$T/notifications") == 2 ]]
tb_scan watch2 >/dev/null; [[ ! -f $path && $(wc -l < "$T/notifications") == 3 ]]; path=$(request)
rm "$T/lost-reply"; gum() { fail 'Saved approval must not ask again'; }
tb_review "$(basename "$path" .json)" >/dev/null
[[ ! -f $path ]]
}
test_disconnect_and_untrusted_text() {
edit_inventory '.devices[0].Name="$(touch /tmp/untrusted)\u001b[2J"'
public_fixture; tb_scan watch1 >/dev/null
[[ $(basename "$(request)") =~ ^request-[0-9a-f]{64}.json$ ]]
[[ $(jq '.devices[0].Name' "$T/inventory" | tb_safe_text) != *$'\e'* ]]
edit_inventory '.devices=[]'; public_fixture; tb_scan watch1 >/dev/null; [[ $(request_count) == 0 ]]
}
test_snapshot_freshness_and_disabled_state() {
# Exercise the real public reader, including freshness after daemon failure.
source "$ROOT/install/helpers/thunderbolt-policy.sh"
TB_RUNTIME="$T/runtime"
jq -cn --argjson now "$(date +%s)" '{available:true,time:$now,generation:"generation-a"}' > "$TB_RUNTIME/snapshot.json"
tb_public_snapshot >/dev/null
jq '.time-=30' "$TB_RUNTIME/snapshot.json" > "$T/next"; mv "$T/next" "$TB_RUNTIME/snapshot.json"
reject tb_public_snapshot
echo generation-b > "$TB_RUNTIME/generation"; reject tb_public_snapshot
}
test_invalid_state_not_skipped_by_migration() {
echo '{"enabled":false}' > "$TB_STATE"; reject tb_admin migrate
[[ ! -s $T/units ]]
}
for case in $(declare -F | awk '$3 ~ /^test_/ {print $3}'); do
(fixture; "$case")
pass "Thunderbolt ${case#test_}"
done
@@ -0,0 +1,31 @@
#!/bin/bash
set -euo pipefail
source "$ROOT/test/shell.d/base-test.sh"
fixture=$(mktemp -d)
trap 'rm -rf -- "$fixture"' EXIT
for name in admin approve daemon; do
# Map the security library and stop before service work, including when the
# suite itself runs as root. Startup checks remain the production code.
sed -e "s|source /usr/bin/omarchy-security-functions|source $ROOT/bin/omarchy-security-functions|" \
-e 's|source /usr/share/omarchy/install/helpers/thunderbolt-policy.sh|exit 126|' \
"$ROOT/bin/omarchy-thunderbolt-authorization-$name" > "$fixture/entry"
chmod 755 "$fixture/entry"
printf 'set -p\n' > "$fixture/decoy"
if BASH_ENV="$fixture/decoy" /usr/bin/bash "$fixture/entry" -p; then
fail "$name accepts ordinary Bash with a decoy -p"
fi
printf 'touch "%s"\n' "$fixture/injected" > "$fixture/startup"
# The entrypoint must suppress startup files and exported functions before
# checking EUID or reaching the fixture's service-work boundary.
if BASH_ENV="$fixture/startup" "$fixture/entry" >/dev/null 2>&1; then
fail "$name passed the fixture service-work boundary"
fi
[[ ! -e $fixture/injected ]] || fail "$name executed BASH_ENV"
if /usr/bin/env 'BASH_FUNC_source%%=() { echo injected > "$TB_TEST_INJECTED"; }' \
TB_TEST_INJECTED="$fixture/injected" "$fixture/entry" >/dev/null 2>&1; then
fail "$name passed the fixture boundary with an exported function"
fi
[[ ! -e $fixture/injected ]] || fail "$name imported an exported function"
pass "Thunderbolt $name rejects unsafe Bash startup"
done
@@ -0,0 +1,57 @@
#include <usbguard/Rule.hpp>
#include <fstream>
#include <iostream>
#include <stdexcept>
#include <string>
static std::string read(const char* path) {
std::ifstream file(path);
std::string text;
std::getline(file, text);
if (text.empty()) throw std::runtime_error("empty rule");
return text;
}
static std::string replace(std::string text, const std::string& from, const std::string& to) {
const auto index = text.find(from);
if (index == std::string::npos) throw std::runtime_error("missing attribute");
text.replace(index, from.size(), to);
return text;
}
static void check(const char* name, const usbguard::Rule& policy, const usbguard::Rule& device, bool expected) {
if (policy.appliesTo(device) != expected) throw std::runtime_error(name);
std::cout << "ok - native USBGuard: " << name << '\n';
}
int main(int argc, char** argv) {
if (argc != 3) return 2;
// Test the policy produced by the real Bash implementation, not a C++ copy.
const auto policy = usbguard::Rule::fromString(read(argv[1]));
const auto text = read(argv[2]);
const auto device = usbguard::Rule::fromString(text);
check("original connection", policy, device, true);
auto moved = device;
moved.setViaPort("9-9.9");
check("another port", policy, moved, true);
moved.setParentHash("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
check("another hub", policy, moved, true);
auto reloaded = usbguard::Rule::fromString(policy.toString());
check("serialized policy reload on new hub", reloaded, moved, true);
check("reconnect on original port", reloaded, device, true);
auto different = moved;
different.setHash("AAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAAA=");
check("different hash denied", policy, different, false);
different = moved;
different.setSerial("different");
check("different serial denied", policy, different, false);
different = moved;
different.setName("different");
check("different name denied", policy, different, false);
check("different product denied", policy, usbguard::Rule::fromString(replace(text, "046d:c53a", "046d:c53b")), false);
check("different vendor denied", policy, usbguard::Rule::fromString(replace(text, "046d:c53a", "046e:c53a")), false);
check("different interface denied", policy, usbguard::Rule::fromString(replace(text, "03:00:00", "08:06:50")), false);
check("different connection type denied", policy, usbguard::Rule::fromString(replace(text, "with-connect-type \"unknown\"", "with-connect-type \"hotplug\"")), false);
// Existing unmarked/manual rules remain location-bound.
check("legacy rule still restricts ports and hubs", device, moved, false);
}
+81 -3
View File
@@ -17,8 +17,18 @@ write_backlights() {
done
}
# A copy of the command reads its platform file from a fixture root: none
# unless a case writes one, whatever the machine running the suite has installed.
platform_root="$tmp_dir/platform"
mkdir -p "$platform_root" "$tmp_dir/bin"
platform_root_copy "$ROOT/bin/omarchy-hw-display" "$tmp_dir/bin/omarchy-hw-display" "$platform_root"
write_displays_conf() {
printf '%s\n' "$@" >"$platform_root/displays.conf"
}
hw_display() {
OMARCHY_BACKLIGHT_PATH="$tmp_dir/backlight" "$ROOT/bin/omarchy-hw-display"
OMARCHY_BACKLIGHT_PATH="$tmp_dir/backlight" "$tmp_dir/bin/omarchy-hw-display"
}
write_backlights intel_backlight
@@ -48,9 +58,9 @@ pass "a T2 Mac uses gmux instead of the Touch Bar"
write_backlights appletb_backlight
if hw_display >/dev/null 2>&1; then
fail "the Touch Bar is never used as the display backlight"
fail "the T2 Touch Bar is never used as the display backlight"
fi
pass "the Touch Bar is never used as the display backlight"
pass "the T2 Touch Bar is never used as the display backlight"
write_backlights acpi_video0 amdgpu_bl0 appletb_backlight gmux_backlight intel_backlight
device=$(hw_display)
@@ -62,3 +72,71 @@ if hw_display >/dev/null 2>&1; then
fail "no backlight device reports failure"
fi
pass "no backlight device reports failure"
# An empty platform file changes nothing.
: >"$platform_root/displays.conf"
write_backlights acpi_video0 amdgpu_bl1
device=$(hw_display)
[[ $device == "amdgpu_bl1" ]] || fail "an empty platform file keeps the candidate order" "actual: $device"
write_backlights nvidia_wmi_ec_backlight
device=$(hw_display)
[[ $device == "nvidia_wmi_ec_backlight" ]] || fail "an empty platform file keeps the fallback" "actual: $device"
pass "an empty platform file changes nothing"
write_displays_conf '# accessory panels' 'backlight-skip accessory-pipe' 'backlight-skip *.dsi.0' 'backlight-prefer panel-bl'
write_backlights 1000.dsi.0 accessory-pipe
if hw_display >/dev/null 2>&1; then
fail "a skipped backlight is never the fallback"
fi
write_backlights 1000.dsi.0 accessory-pipe zz-other
device=$(hw_display)
[[ $device == "zz-other" ]] || fail "the fallback passes over skipped backlights" "actual: $device"
pass "a platform's skipped backlights are never the fallback"
write_backlights 1000.dsi.0 amdgpu_bl0 panel-bl
device=$(hw_display)
[[ $device == "panel-bl" ]] || fail "a preferred backlight beats the GPU backlights" "actual: $device"
printf 'backlight-prefer panel-bl\r\n' >"$platform_root/displays.conf"
device=$(hw_display)
[[ $device == "panel-bl" ]] || fail "a file saved with CRLF line ends reads the same" "actual: $device"
write_displays_conf '# accessory panels' 'backlight-skip accessory-pipe' 'backlight-skip *.dsi.0' 'backlight-prefer panel-bl'
write_backlights gmux_backlight panel-bl
device=$(hw_display)
[[ $device == "gmux_backlight" ]] || fail "gmux still beats a preferred backlight" "actual: $device"
pass "a platform's preferred backlight follows gmux and beats the GPU backlights"
write_displays_conf 'backlight-prefer second-bl' 'backlight-prefer first-bl'
write_backlights first-bl second-bl
device=$(hw_display)
[[ $device == "second-bl" ]] || fail "preferences are tried in file order" "actual: $device"
pass "preferences are tried in file order"
write_displays_conf 'backlight-skip intel_backlight' 'backlight-prefer panel-bl' 'backlight-skip panel-bl'
write_backlights acpi_video0 intel_backlight panel-bl
device=$(hw_display)
[[ $device == "acpi_video0" ]] || fail "a skip beats preferred and built-in candidates" "actual: $device"
write_displays_conf 'backlight-skip gmux_backlight'
write_backlights gmux_backlight intel_backlight
device=$(hw_display)
[[ $device == "intel_backlight" ]] || fail "a skip beats gmux" "actual: $device"
pass "a skip beats preferred and built-in candidates"
write_displays_conf 'backlight-prefer panel-*' 'backlight-prefer .' 'backlight-prefer ..'
write_backlights amdgpu_bl0 panel-bl
device=$(hw_display)
[[ $device == "amdgpu_bl0" ]] || fail "a preference is a literal name" "actual: $device"
pass "a preference is a literal name"
# Only whole directives count: an unknown one, a path, an extra word or a
# trailing comment leaves the line out; the last line needs no newline.
printf '%s\n' 'backlight-future panel-bl' 'backlight-prefer ../panel-bl' 'backlight-prefer panel-bl extra' \
'backlight-skip acpi_video0 # comment' >"$platform_root/displays.conf"
printf 'backlight-prefer last-bl' >>"$platform_root/displays.conf"
write_backlights acpi_video0 last-bl panel-bl
device=$(hw_display)
[[ $device == "last-bl" ]] || fail "malformed directives are ignored and the last line is read" "actual: $device"
write_displays_conf 'backlight-skip acpi_video0 # comment'
write_backlights acpi_video0
device=$(hw_display)
[[ $device == "acpi_video0" ]] || fail "a directive with a trailing comment is ignored" "actual: $device"
pass "malformed directives are ignored and the last line is read"
+141
View File
@@ -0,0 +1,141 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
acpi_lid="$test_tmp/acpi/button/lid"
input_class="$test_tmp/input"
dmi_chassis="$test_tmp/chassis_type"
mkdir -p "$stub_bin" "$acpi_lid/LID0" "$input_class"
busctl_log="$test_tmp/busctl.log"
cat >"$stub_bin/busctl" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$BUSCTL_LOG"
[[ ${OMARCHY_TEST_LID_STATE:-} != "fail" ]] || exit 1
printf '%s\n' "${OMARCHY_TEST_LID_STATE:-}"
SH
chmod +x "$stub_bin/busctl"
run_laptop() {
OMARCHY_DMI_CHASSIS_TYPE_PATH="$dmi_chassis" \
OMARCHY_ACPI_LID_PATH="$acpi_lid" \
OMARCHY_INPUT_CLASS_PATH="$input_class" \
PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-hw-laptop"
}
run_lid_closed() {
OMARCHY_ACPI_LID_PATH="$acpi_lid" \
OMARCHY_INPUT_CLASS_PATH="$input_class" \
BUSCTL_LOG="$busctl_log" \
PATH="$stub_bin:$PATH" \
OMARCHY_TEST_LID_STATE="$1" \
"$ROOT/bin/omarchy-hw-laptop-closed"
}
# Input devices as sysfs shows them: a name and a switch capability bitmap
# whose last word holds SW_LID in bit 0.
input_device() {
mkdir -p "$input_class/$1/capabilities"
printf '%s\n' "$2" >"$input_class/$1/name"
printf '%s\n' "$3" >"$input_class/$1/capabilities/sw"
}
# No ACPI lid, no lid switch, desktop chassis: a Mac mini, a Studio or a tower.
: >"$acpi_lid/LID0/state"
rm -f "$acpi_lid/LID0/state"
printf '3\n' >"$dmi_chassis"
input_device input0 "Power Button" 0
input_device input1 "Apple SMC power/lid events" 0
input_device input2 "MacBook Pro J416 Headphone Jack" 14
if run_laptop; then
fail "a desktop without a lid is not classified as a laptop"
fi
pass "a desktop without a lid is not classified as a laptop, even with the SMC's power device"
# Apple Silicon MacBook: no ACPI lid, but the SMC reports a lid switch.
input_device input1 "Apple SMC power/lid events" 1
run_laptop || fail "an Apple SMC lid switch is classified as a laptop"
pass "an Apple SMC lid switch is classified as a laptop"
input_device input1 "Lid Switch" "0 1"
run_laptop || fail "a lid switch in the low word of a longer bitmap is classified as a laptop"
pass "a lid switch in the low word of a longer bitmap is classified as a laptop"
input_device input1 "Lid Switch" "1 0"
if run_laptop; then
fail "a switch bit in a higher word is not a lid"
fi
pass "a switch bit in a higher word is not a lid"
# x86 laptop via DMI when ACPI and evdev lids are absent.
input_device input1 "Power Button" 0
printf '9\n' >"$dmi_chassis"
run_laptop || fail "DMI laptop chassis is recognized when ACPI is absent"
pass "DMI laptop chassis is recognized when ACPI is absent"
printf '3\n' >"$dmi_chassis"
if run_laptop; then
fail "a desktop DMI chassis is not classified as a laptop"
fi
pass "a desktop DMI chassis is not classified as a laptop"
printf 'closed\n' >"$acpi_lid/LID0/state"
run_laptop || fail "an ACPI lid is classified as a laptop"
pass "an ACPI lid is classified as a laptop"
# An ACPI lid answers for itself, as it always has, without asking logind over
# D-Bus: this runs in sudo's PAM stack where fingerprint login is set up.
: >"$busctl_log"
run_lid_closed 'b false' ||
fail "an ACPI lid reports itself closed"
pass "an ACPI lid reports itself closed"
printf 'open\n' >"$acpi_lid/LID0/state"
if run_lid_closed 'b true'; then
fail "an ACPI lid reports itself open"
fi
pass "an ACPI lid reports itself open"
[[ ! -s $busctl_log ]] || fail "an ACPI lid leaves logind unasked" "$(cat "$busctl_log")"
pass "an ACPI lid leaves logind unasked"
# Without an ACPI lid, logind answers only for a lid switch it can track: it
# says "open" of a machine it tracks no lid on.
rm -f "$acpi_lid/LID0/state"
input_device input1 "Power Button" 0
if run_lid_closed 'b true'; then
fail "a machine without a lid is never closed"
fi
[[ ! -s $busctl_log ]] || fail "a machine without a lid leaves logind unasked" "$(cat "$busctl_log")"
pass "a machine without a lid is never closed and leaves logind unasked"
input_device input1 "Apple SMC power/lid events" 1
run_lid_closed 'b true' ||
fail "logind recognizes a closed lid switch"
pass "logind recognizes a closed lid switch"
[[ $(head -1 "$busctl_log") == "--timeout=2 "*" LidClosed" ]] ||
fail "logind gets a short timeout, not busctl's 25 s" "$(cat "$busctl_log")"
pass "logind gets a short timeout, not busctl's 25 s"
if run_lid_closed 'b false'; then
fail "logind recognizes an open lid switch"
fi
pass "logind recognizes an open lid switch"
input_device input1 "Lid Switch" "0 1"
run_lid_closed 'b true' ||
fail "a lid switch in the low word of a longer bitmap is asked about"
pass "a lid switch in the low word of a longer bitmap is asked about"
for answer in '' 'not-a-property' 'fail'; do
if run_lid_closed "$answer"; then
fail "a logind that does not answer leaves the lid open" "$answer"
fi
done
pass "a logind that does not answer leaves the lid open"
+203
View File
@@ -0,0 +1,203 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
detector="$ROOT/bin/omarchy-hw-platform"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
for platform in aarch64-apple aarch64 x86; do
fake_platform "$test_tmp/$platform" "$platform"
done
# A privileged caller reads the live device tree and the system uname, never
# the fixture roots or PATH its environment names. Run it as root when the suite
# is root and as namespaced root otherwise.
root_runner=()
if (( EUID != 0 )); then
root_runner=(unshare --user --map-root-user)
fi
if (( EUID == 0 )) || unshare --user --map-root-user true 2>/dev/null; then
live=$("${root_runner[@]}" "$detector") || fail "root detects the live platform"
[[ $live =~ ^(aarch64-apple|aarch64|x86)$ ]] || fail "root detects the live platform" "live: $live"
for platform in aarch64-apple aarch64 x86; do
fixture="$test_tmp/$platform"
if [[ -f $fixture/proc/device-tree/compatible ]]; then
mkdir -p "$fixture/sys/firmware/devicetree/base"
cp "$fixture/proc/device-tree/compatible" "$fixture/sys/firmware/devicetree/base/compatible"
fi
overridden=$(OMARCHY_PROC_ROOT="$fixture/proc" OMARCHY_SYS_ROOT="$fixture/sys" PATH="$fixture/bin:$ROOT/bin:$PATH" \
"${root_runner[@]}" "$detector") || fail "root detects the live platform with a $platform fixture in its environment"
[[ $overridden == "$live" ]] ||
fail "root ignores a $platform fixture in its environment" "live: $live
with fixture: $overridden"
done
pass "root ignores fixture roots and PATH when detecting the platform"
else
skip "no unprivileged user namespace; skipping the root override probe"
fi
# -p in the shebang is what keeps exported functions and BASH_ENV out, so an
# ordinary Bash launch with a decoy -p argument is refused before it reads anything.
for command in omarchy-hw-platform omarchy-hw-aarch64-apple omarchy-hw-apple-silicon; do
if /usr/bin/bash "$ROOT/bin/$command" -p >/dev/null 2>"$test_tmp/error"; then
fail "$command refuses an ordinary Bash launch"
fi
grep -Fq "Refusing an unsafe Bash startup" "$test_tmp/error" || fail "$command explains the refusal" "$(cat "$test_tmp/error")"
done
pass "the detector and the Apple predicate refuse an ordinary Bash launch with a decoy -p"
require_platform_fixtures "the platform fixtures"
# The three platforms every caller is written against.
for platform in aarch64-apple aarch64 x86; do
fixture="$test_tmp/$platform"
actual=$(OMARCHY_PROC_ROOT="$fixture/proc" PATH="$fixture/bin:$ROOT/bin:$PATH" "$detector") ||
fail "the $platform fixture is detected"
[[ $actual == "$platform" ]] || fail "the $platform fixture is detected" "actual: $actual"
apple_status=0
OMARCHY_PROC_ROOT="$fixture/proc" PATH="$fixture/bin:$ROOT/bin:$PATH" "$ROOT/bin/omarchy-hw-aarch64-apple" || apple_status=$?
if [[ $platform == "aarch64-apple" ]]; then
(( apple_status == 0 )) || fail "the Apple predicate accepts the Apple fixture"
else
(( apple_status != 0 )) || fail "the Apple predicate rejects the $platform fixture"
fi
pass "the $platform fixture is detected and the Apple predicate agrees"
done
# Systemd and the shell run the predicate by absolute path with whatever PATH
# they have; it must use the detector shipped beside it.
fixture="$test_tmp/aarch64-apple"
OMARCHY_PROC_ROOT="$fixture/proc" PATH="$fixture/bin:/usr/bin:/bin" "$ROOT/bin/omarchy-hw-aarch64-apple" ||
fail "the Apple predicate finds its detector without Omarchy on PATH"
pass "the Apple predicate finds its detector without Omarchy on PATH"
stub_bin="$test_tmp/bin"
mkdir -p "$stub_bin"
cat >"$stub_bin/uname" <<'SH'
#!/bin/bash
[[ ${1:-} == -m ]] && { printf '%s\n' "${TEST_ARCH:-aarch64}"; exit 0; }
exec /usr/bin/uname "$@"
SH
chmod +x "$stub_bin/uname"
# $1 names a case directory holding proc/ and sys/ roots; $2 is the CPU.
detect() {
local case_dir="$test_tmp/cases/$1"
TEST_ARCH="${2:-aarch64}" OMARCHY_PROC_ROOT="$case_dir/proc" OMARCHY_SYS_ROOT="$case_dir/sys" \
PATH="$stub_bin:$ROOT/bin:$PATH" "$detector" 2>"$test_tmp/error"
}
# $1 case, $2 proc|sys, then the compatible tokens.
write_tree() {
local case_dir="$test_tmp/cases/$1" tree="$2" file
shift 2
mkdir -p "$case_dir/proc" "$case_dir/sys"
if [[ $tree == "proc" ]]; then
file="$case_dir/proc/device-tree/compatible"
else
file="$case_dir/sys/firmware/devicetree/base/compatible"
fi
mkdir -p "$(dirname "$file")"
printf '%s\0' "$@" >"$file"
}
expect() {
local name="$1" machine="$2" expected="$3" description="$4" actual
actual=$(detect "$name" "$machine") || fail "$description" "$(cat "$test_tmp/error")"
[[ $actual == "$expected" ]] || fail "$description" "expected: $expected
actual: $actual"
}
expect_contradiction() {
local name="$1" machine="$2" description="$3"
if detect "$name" "$machine" >/dev/null; then
fail "$description"
fi
grep -Fq "contradictory platform identity" "$test_tmp/error" || fail "$description explains itself" "$(cat "$test_tmp/error")"
}
# Real boards: the M1 Pro and M2 Max test Macs, an M1 Mac mini, and the
# Snapdragon X laptops Dragon supports.
write_tree m1-pro proc apple,j314s apple,t6000 apple,arm-platform
write_tree m2-max proc apple,j416c apple,t6021 apple,arm-platform
write_tree m1-mini proc apple,j274 apple,t8103 apple,arm-platform
write_tree yoga-slim7x proc lenovo,yoga-slim7x qcom,x1e80100
write_tree xps13-9345 proc dell,xps13-9345 qcom,x1e80100
write_tree t14s proc lenovo,thinkpad-t14s qcom,x1e78100 qcom,x1e80100
for board in m1-pro m2-max m1-mini; do
expect "$board" aarch64 aarch64-apple "the $board device tree is Apple Silicon"
done
for board in yoga-slim7x xps13-9345 t14s; do
expect "$board" aarch64 aarch64 "the $board device tree is plain aarch64, never Apple Silicon"
done
pass "real Apple and Snapdragon device trees are recognised"
write_tree qemu-virt proc linux,dummy-virt
write_tree raspberry-pi proc raspberrypi,5-model-b brcm,bcm2712
mkdir -p "$test_tmp/cases/acpi/proc" "$test_tmp/cases/acpi/sys"
expect qemu-virt aarch64 aarch64 "a QEMU virt board is plain aarch64"
expect raspberry-pi aarch64 aarch64 "a Raspberry Pi is plain aarch64"
expect acpi aarch64 aarch64 "an aarch64 machine without a device tree is plain aarch64"
expect acpi x86_64 x86 "an x86 machine without a device tree is x86"
pass "unknown aarch64 and x86 machines need no family"
# Only a token's vendor prefix identifies the board; the old detector matched
# "apple," anywhere in the file.
write_tree substring proc pineapple,board acmeqcom,soc vendor,apple,x vendor,qcom,y
expect substring aarch64 aarch64 "vendor names inside other tokens do not identify the board"
pass "only a token's vendor prefix identifies the board"
# /proc/device-tree is a link into sysfs; read sysfs when it is missing.
write_tree sysfs-qualcomm sys lenovo,yoga-slim7x qcom,x1e80100
write_tree sysfs-apple sys apple,j314s apple,t6000 apple,arm-platform
expect sysfs-qualcomm aarch64 aarch64 "sysfs reads a Snapdragon tree without /proc/device-tree"
expect sysfs-apple aarch64 aarch64-apple "sysfs identifies Apple Silicon without /proc/device-tree"
write_tree agree proc apple,j314s apple,t6000 apple,arm-platform
write_tree agree sys apple,j314s apple,t6000 apple,arm-platform
expect agree aarch64 aarch64-apple "matching proc and sysfs trees agree"
pass "sysfs is the fallback for the device tree"
write_tree both-vendors proc apple,j314s qcom,x1e80100
expect_contradiction both-vendors aarch64 "a device tree naming Apple and Qualcomm fails"
write_tree sources-disagree proc apple,j314s apple,t6000 apple,arm-platform
write_tree sources-disagree sys lenovo,yoga-slim7x qcom,x1e80100
expect_contradiction sources-disagree aarch64 "proc and sysfs naming different vendors fails"
write_tree vendor-vs-none proc apple,j314s apple,t6000 apple,arm-platform
write_tree vendor-vs-none sys linux,dummy-virt
expect_contradiction vendor-vs-none aarch64 "proc naming Apple while sysfs names nobody fails"
expect_contradiction m1-pro x86_64 "an Apple device tree on an x86 CPU fails"
expect_contradiction yoga-slim7x x86_64 "a Qualcomm device tree on an x86 CPU fails"
if TEST_ARCH=x86_64 OMARCHY_PROC_ROOT="$test_tmp/cases/m1-pro/proc" PATH="$stub_bin:$ROOT/bin:$PATH" \
"$ROOT/bin/omarchy-hw-aarch64-apple" 2>/dev/null; then
fail "the Apple predicate fails closed on contradictory identity"
fi
pass "contradictory identity fails with an explanation"
failing_uname="$test_tmp/failing-uname"
mkdir -p "$failing_uname"
printf '#!/bin/bash\nexit 1\n' >"$failing_uname/uname"
chmod +x "$failing_uname/uname"
if OMARCHY_PROC_ROOT="$test_tmp/cases/acpi/proc" PATH="$failing_uname:$PATH" "$detector" 2>"$test_tmp/error"; then
fail "an unreadable CPU architecture fails instead of guessing generic"
fi
grep -Fq "cannot read the CPU architecture" "$test_tmp/error" || fail "an unreadable CPU architecture explains itself" "$(cat "$test_tmp/error")"
pass "an unreadable CPU architecture fails instead of guessing generic"
# The architecture predicates answer from the CPU alone: aarch64 covers every
# ARM family, Apple Silicon included, and x86 none of them.
for platform in x86 aarch64 aarch64-apple; do
fake_platform "$test_tmp/arch-$platform" "$platform"
arch_path="$test_tmp/arch-$platform/bin:$ROOT/bin:$PATH"
if [[ $platform == "x86" ]]; then
PATH=$arch_path omarchy-hw-x86 || fail "x86: omarchy-hw-x86 accepts it"
! PATH=$arch_path omarchy-hw-aarch64 || fail "x86: omarchy-hw-aarch64 rejects it"
else
PATH=$arch_path omarchy-hw-aarch64 || fail "$platform: omarchy-hw-aarch64 accepts it"
! PATH=$arch_path omarchy-hw-x86 || fail "$platform: omarchy-hw-x86 rejects it"
fi
done
pass "omarchy-hw-x86 and omarchy-hw-aarch64 split every platform by its CPU"
+97
View File
@@ -0,0 +1,97 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
touchpad="$ROOT/bin/omarchy-hw-touchpad"
require_command jq
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
input_class="$test_tmp/input"
mkdir -p "$stub_bin"
# Hyprland's mice, in its order, from a space-separated MICE.
cat >"$stub_bin/hyprctl" <<'SH'
#!/bin/bash
jq -n --arg names "${MICE:-}" \
'{mice: [$names | split(" ")[] | select(. != "") | {name: .}], keyboards: []}'
SH
# udev's properties for an event device, kept beside it in the fixture.
cat >"$stub_bin/udevadm" <<'SH'
#!/bin/bash
cat "${!#}/udev" 2>/dev/null
SH
chmod +x "$stub_bin"/*
# An event device as sysfs shows it: its input device's name, and what udev
# made of it.
event_device() {
mkdir -p "$input_class/$1/device"
printf '%s\n' "$2" >"$input_class/$1/device/name"
printf '%s\n' "$3" >"$input_class/$1/udev"
}
detected() {
MICE="$1" OMARCHY_INPUT_CLASS_PATH="$input_class" PATH="$stub_bin:$PATH" bash "$touchpad"
}
# Apple Silicon's trackpad is named after its MTP HID interface, so neither
# word is in its name; udev still calls it a touchpad.
event_device event1 "Apple MTP keyboard" "ID_INPUT_KEYBOARD=1"
event_device event2 "Apple MTP multi-touch" "ID_INPUT_TOUCHPAD=1"
[[ $(detected "apple-mtp-multi-touch") == "apple-mtp-multi-touch" ]] ||
fail "the Apple Silicon trackpad is detected" "$(detected "apple-mtp-multi-touch")"
pass "the Apple Silicon trackpad is detected"
# omarchy-toggle-input-device passes the name straight to hl.device, so a name
# that does not match leaves the menu entry hidden and the toggle dead.
for name in "elan-touchpad" "apple-magic-trackpad-2"; do
[[ $(detected "$name") == "$name" ]] || fail "the touchpads that already worked still do" "$(detected "$name")"
done
pass "the touchpads that already worked still do"
# Some touchscreens' mouse interfaces are called multi-touch as well. A named
# touchpad wins over one listed first, and without one it takes udev calling
# the device a touchpad, not its name.
rm -rf "$input_class"
event_device event5 "ELAN9008:00 04F3:2C82 Multi-Touch" "ID_INPUT_TOUCHSCREEN=1"
event_device event7 "SYNA2BA6:00 06CB:CEF5 Touchpad" "ID_INPUT_TOUCHPAD=1"
[[ $(detected "elan9008:00-04f3:2c82-multi-touch syna2ba6:00-06cb:cef5-touchpad") == "syna2ba6:00-06cb:cef5-touchpad" ]] ||
fail "a touchscreen listed first does not take the touchpad's place" "$(detected "elan9008:00-04f3:2c82-multi-touch syna2ba6:00-06cb:cef5-touchpad")"
[[ -z $(detected "elan9008:00-04f3:2c82-multi-touch") ]] ||
fail "a multi-touch touchscreen alone is no touchpad" "$(detected "elan9008:00-04f3:2c82-multi-touch")"
pass "a multi-touch touchscreen is never taken for the touchpad"
# Hyprland turns commas into dashes as well as spaces.
event_device event9 "Acme, Inc. Precision Pad" "ID_INPUT_TOUCHPAD=1"
[[ $(detected "acme--inc.-precision-pad") == "acme--inc.-precision-pad" ]] ||
fail "a touchpad named with a comma is found by Hyprland's name for it" "$(detected "acme--inc.-precision-pad")"
pass "a touchpad named with a comma is found by Hyprland's name for it"
# Hyprland adds -1, -2 and so on to a name another device already took, as when
# the MTP interface exposes the trackpad twice; that name is the one to toggle,
# and an unrelated name that merely starts the same is not.
event_device event11 "Apple MTP multi-touch" "ID_INPUT_TOUCHPAD=1"
[[ $(detected "apple-mtp-multi-touch-1") == "apple-mtp-multi-touch-1" ]] ||
fail "a touchpad under Hyprland's numbered name is found" "$(detected "apple-mtp-multi-touch-1")"
[[ -z $(detected "apple-mtp-multi-touch-extra") ]] ||
fail "a name that only starts with the touchpad's is not taken for it" "$(detected "apple-mtp-multi-touch-extra")"
pass "a touchpad under Hyprland's numbered name for a duplicate is found"
# udev's touchpad has to be one of Hyprland's mice for its name to toggle.
[[ -z $(detected "logitech-mx-master-3") ]] ||
fail "a udev touchpad Hyprland does not list is not reported" "$(detected "logitech-mx-master-3")"
pass "an ordinary mouse is not mistaken for a touchpad"
[[ -z $(detected "") ]] ||
fail "no touchpad answers with an empty detection" "$(detected "")"
pass "no touchpad answers with an empty detection"
@@ -151,7 +151,9 @@ cat > "$stub_bin/omarchy-default-dictation" <<'SH'
#!/bin/bash
echo voxtype
SH
chmod +x "$stub_bin/omarchy-default-dictation"
# The dictation keys are bound only once the selected backend is installed.
printf '#!/bin/bash\nexit 0\n' > "$stub_bin/voxtype"
chmod +x "$stub_bin/omarchy-default-dictation" "$stub_bin/voxtype"
bindings=$(PATH="$stub_bin:$PATH" list_bindings "$home")
[[ -n $bindings ]] || fail "default bindings load for the conflict check"
+5 -1
View File
@@ -158,6 +158,10 @@ echo future-backend
SH
chmod +x "$missing_bin/omarchy-default-dictation"
dictation_output=$(PATH="$missing_bin" run_omarchy_bindings "$dictation_home")
if grep -Fq 'dictation' <<<"$dictation_output"; then fail "a selected backend that is not installed leaves application shortcuts available"; fi
printf '#!/bin/bash\nexit 0\n' > "$missing_bin/future-backend"
chmod +x "$missing_bin/future-backend"
dictation_output=$(PATH="$missing_bin" run_omarchy_bindings "$dictation_home")
for binding in \
$'SUPER + CTRL + X\tToggle dictation' \
$'F9\tStart dictation (push-to-talk)' \
@@ -166,7 +170,7 @@ for binding in \
$'ALT + Alt_R\tStop dictation (push-to-talk)'; do
grep -Fq "$binding" <<<"$dictation_output" || fail "dictation bindings load for any selected backend" "$binding"
done
pass "dictation bindings require a selection without hardcoding backends"
pass "dictation bindings require an installed selection without hardcoding backends"
# Lazydocker is optional on new installs, while existing installs keep the hotkey.
lazydocker_bin="$tmpdir/lazydocker-bin"
@@ -0,0 +1,665 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Root never reads the fixture root, so as root the build and first boot below
# would act on this machine. A skip is a pass.
if (( EUID == 0 )); then
skip "running as root, where image setup ignores fixture roots; skipping"
exit 0
fi
umask 022
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
unit_name=omarchy-provision-hardware.service
unit_source="$ROOT/install/provisioning/$unit_name"
# omarchy-apply-hardware refuses anyone but root, and root never reads the
# fixture root. Run a copy whose only change is that check, so a normal user
# can build an image into a fixture root.
apply_hardware="$test_tmp/omarchy-apply-hardware"
sed 's/^if (( EUID != 0 )); then$/if false; then/' "$ROOT/bin/omarchy-apply-hardware" >"$apply_hardware"
chmod +x "$apply_hardware"
grep -q '^if false; then$' "$apply_hardware" || fail "the test copy of omarchy-apply-hardware drops only its root check"
# Commands a hardware leaf would reach for. Any call means a leaf ran.
stub_bin="$test_tmp/stub-bin"
mkdir -p "$stub_bin"
for command in sudo pacman pacman-key systemctl lspci omarchy-pkg-add modinfo; do
cat >"$stub_bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$LEAF_CALLS"
exit 1
SH
done
stub_rebuild() {
cat >"$stub_bin/$1" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$REBUILDS"
SH
chmod +x "$stub_bin/$1"
}
stub_rebuild mkinitcpio
# Only the stubs count as present, so a machine with Limine never runs its own
# limine-mkinitcpio here.
cat >"$stub_bin/omarchy-cmd-present" <<SH
#!/bin/bash
[[ -x $stub_bin/\$1 ]]
SH
chmod +x "$stub_bin"/*
export LEAF_CALLS="$test_tmp/leaf-calls" REBUILDS="$test_tmp/rebuilds"
fake_platform "$test_tmp/hw" aarch64-apple
base_path="$test_tmp/hw/bin:$stub_bin:$ROOT/bin:/usr/local/bin:/usr/bin:/bin"
# An Omarchy tree whose hardware setup is three leaves that log their runs.
# Leaves a and b write initramfs drop-ins; b then fails while $FAIL_B exists.
fixture="$test_tmp/omarchy"
mkdir -p "$fixture/install/helpers" "$fixture/install/provisioning" "$fixture/install/hardware/apple" "$fixture/bin"
cp "$ROOT/install/helpers/logging.sh" "$ROOT/install/helpers/image-target.sh" "$fixture/install/helpers/"
cp "$unit_source" "$fixture/install/provisioning/"
cat >"$fixture/install/hardware/all.sh" <<'SH'
run_logged "$OMARCHY_INSTALL/hardware/a.sh"
run_logged "$OMARCHY_INSTALL/hardware/apple/b.sh"
run_logged "$OMARCHY_INSTALL/hardware/c.sh"
SH
cat >"$fixture/install/hardware/a.sh" <<'SH'
printf 'a user=%s path=%s\n' "${OMARCHY_INSTALL_USER-unset}" "$OMARCHY_PATH" >>"$RUNS"
mkdir -p "$OMARCHY_IMAGE_ROOT/etc/mkinitcpio.conf.d"
echo 'MODULES+=(a)' >"$OMARCHY_IMAGE_ROOT/etc/mkinitcpio.conf.d/a.conf"
SH
cat >"$fixture/install/hardware/apple/b.sh" <<'SH'
mkdir -p "$OMARCHY_IMAGE_ROOT/etc/mkinitcpio.conf.d"
echo 'MODULES+=(b)' >"$OMARCHY_IMAGE_ROOT/etc/mkinitcpio.conf.d/b.conf"
[[ ! -e $FAIL_B ]] || return 1
echo b >>"$RUNS"
SH
cat >"$fixture/install/hardware/c.sh" <<'SH'
echo c >>"$RUNS"
[[ ! -e $REQUEST_REBUILD ]] || touch "$OMARCHY_IMAGE_BOOT_REBUILD"
SH
export RUNS="$test_tmp/runs" FAIL_B="$test_tmp/fail-b" REQUEST_REBUILD="$test_tmp/request-rebuild"
new_root() {
local root="$test_tmp/root-$1"
rm -rf "$root"
mkdir -p "$root/var/log"
printf '%s\n' "$root"
}
write_manifest() {
local root=$1 body=${2:-$'format=1\nplatform=aarch64-apple\n'}
mkdir -p "$root/var/lib/omarchy/image"
chmod 0755 "$root/var/lib/omarchy/image"
printf '%s' "$body" >"$root/var/lib/omarchy/image/target"
chmod 0644 "$root/var/lib/omarchy/image/target"
}
build() {
local root=$1 omarchy=${2:-$fixture}
OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$omarchy" OMARCHY_INSTALL_LOG_FILE="$root/var/log/omarchy-install.log" \
PATH="$base_path" "$apply_hardware" --defer-provisioning
}
first_boot() {
local root=$1
OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$fixture" OMARCHY_PROC_ROOT="$test_tmp/hw/proc" \
PATH="$base_path" "$ROOT/bin/omarchy-provision-hardware"
}
reset_logs() {
rm -f "$RUNS" "$LEAF_CALLS" "$REBUILDS" "$FAIL_B"
}
queue_of() {
cat "$1/var/lib/omarchy/image/deferred-steps"
}
# --- The build ---------------------------------------------------------------
# The real hardware setup: every leaf is queued in order and none of them runs.
reset_logs
root=$(new_root real)
write_manifest "$root"
output=$(build "$root" "$ROOT") || fail "an image build defers the real hardware setup" "$output"
expected=$(sed -n 's|^run_logged "\$OMARCHY_INSTALL/\(hardware/[^"]*\)"$|install/\1|p' "$ROOT/install/hardware/all.sh")
[[ -n $expected && $(queue_of "$root") == "$expected" ]] ||
fail "an image build queues every hardware leaf in order" "expected:
$expected
queued:
$(queue_of "$root")"
[[ ! -e $LEAF_CALLS ]] || fail "an image build runs no hardware leaf" "$(cat "$LEAF_CALLS")"
[[ $output == *"Image build for aarch64-apple: deferred $(wc -l <<<"$expected") hardware steps to first boot"* ]] ||
fail "an image build reports what it deferred" "$output"
pass "an image build queues every hardware leaf in order and runs none of them"
[[ $(<"$unit_source") == "$(<"$root/etc/systemd/system/$unit_name")" ]] ||
fail "an image build installs the first-boot hardware service"
[[ $(readlink "$root/etc/systemd/system/multi-user.target.wants/$unit_name") == "/etc/systemd/system/$unit_name" ]] ||
fail "an image build enables the first-boot hardware service"
[[ -f $root/var/lib/omarchy/image/target && ! -e $root/var/lib/omarchy/image/target.booted ]] ||
fail "an image build leaves its manifest for the first boot"
[[ $(stat -c '%a' "$root/var/lib/omarchy/image/deferred-steps") == "644" ]] ||
fail "the queue is readable and only its owner writes it"
pass "an image build arms the first-boot hardware service"
before=$(queue_of "$root")
build "$root" "$ROOT" >/dev/null || fail "an image build can run hardware setup again"
[[ $(queue_of "$root") == "$before" ]] || fail "running hardware setup again in the build queues the same steps"
pass "running hardware setup again in the build queues the same steps"
# Without a manifest nothing is deferred: the leaves run as they always did.
reset_logs
root=$(new_root live)
build "$root" >/dev/null || fail "hardware setup without a manifest runs"
[[ $(cat "$RUNS") == $'a user= path='"$fixture"$'\nb\nc' ]] || fail "hardware setup without a manifest runs every leaf" "$(cat "$RUNS")"
[[ ! -e $root/var/lib/omarchy/image && ! -e $root/etc/systemd/system/$unit_name ]] ||
fail "hardware setup without a manifest queues and arms nothing"
pass "hardware setup without a manifest runs every leaf and defers nothing"
# Hardware setup that has to wait for a first boot in progress decides only
# once it has the lock: by then the manifest is retired and the leaves run here.
reset_logs
root=$(new_root race)
write_manifest "$root"
(
exec {lock}>>"$root/var/lib/omarchy/image/lock"
flock "$lock"
touch "$test_tmp/locked"
until [[ -e $test_tmp/release || ! -d $test_tmp ]]; do sleep 0.1; done
mv "$root/var/lib/omarchy/image/target" "$root/var/lib/omarchy/image/target.booted"
) &
first_boot_pid=$!
until [[ -e $test_tmp/locked ]]; do sleep 0.1; done
build "$root" >/dev/null &
setup_pid=$!
sleep 1
kill -0 "$setup_pid" 2>/dev/null || fail "hardware setup waits while a first boot holds the lock"
touch "$test_tmp/release"
wait "$first_boot_pid"
wait "$setup_pid" || fail "hardware setup runs after waiting for a first boot"
[[ $(cat "$RUNS") == $'a user= path='"$fixture"$'\nb\nc' && ! -e $root/var/lib/omarchy/image/deferred-steps ]] ||
fail "hardware setup that waited for a first boot runs its leaves instead of queueing them"
pass "hardware setup waits for a first boot in progress and then runs on the machine"
# A manifest that is not unambiguously root's own stops hardware setup outright.
check_refused() {
local description=$1 root=$2
reset_logs
if build "$root" >/dev/null 2>&1; then
fail "hardware setup refuses $description"
fi
[[ ! -e $RUNS && ! -e $root/var/lib/omarchy/image/deferred-steps && ! -e $root/etc/systemd/system/$unit_name ]] ||
fail "hardware setup refuses $description without running, queueing or arming anything"
pass "hardware setup refuses $description"
}
root=$(new_root format)
write_manifest "$root" $'format=2\nplatform=aarch64-apple\n'
check_refused "a manifest of an unknown format" "$root"
root=$(new_root platform)
write_manifest "$root" $'format=1\nplatform=apple\n'
check_refused "a manifest naming an unknown platform" "$root"
root=$(new_root malformed)
write_manifest "$root" $'format=1\napple-silicon\n'
check_refused "a malformed manifest" "$root"
root=$(new_root writable)
write_manifest "$root"
chmod 0666 "$root/var/lib/omarchy/image/target"
check_refused "a manifest others can write" "$root"
root=$(new_root writable-dir)
write_manifest "$root"
chmod 0777 "$root/var/lib/omarchy/image"
check_refused "a manifest in a directory others can write" "$root"
root=$(new_root symlink)
write_manifest "$root"
mv "$root/var/lib/omarchy/image/target" "$root/var/lib/omarchy/image/real-target"
ln -s real-target "$root/var/lib/omarchy/image/target"
check_refused "a symlinked manifest" "$root"
# --- The first boot ----------------------------------------------------------
reset_logs
root=$(new_root fresh)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
[[ $(queue_of "$root") == $'install/hardware/a.sh\ninstall/hardware/apple/b.sh\ninstall/hardware/c.sh' ]] ||
fail "the fixture image queues its three leaves" "$(queue_of "$root")"
output=$(first_boot "$root") || fail "the first boot finishes the deferred hardware setup" "$output"
[[ $(cat "$RUNS") == $'a user= path='"$fixture"$'\nb\nc' ]] ||
fail "the first boot runs each deferred leaf once, in order, with no install user" "$(cat "$RUNS")"
[[ $output == *"First boot of an image built for aarch64-apple, on aarch64-apple hardware"* ]] ||
fail "the first boot reports the image target and the live platform" "$output"
[[ ! -e $root/var/lib/omarchy/image/target && -f $root/var/lib/omarchy/image/target.booted ]] ||
fail "the first boot retires the build manifest"
[[ ! -e $root/var/lib/omarchy/image/deferred-steps ]] || fail "the first boot empties the queue"
[[ ! -L $root/etc/systemd/system/multi-user.target.wants/$unit_name && -f $root/etc/systemd/system/$unit_name ]] ||
fail "the first boot disables its service and keeps the unit, so a start job already queued is skipped rather than failed"
[[ $(cat "$REBUILDS") == "mkinitcpio -P" ]] ||
fail "the first boot rebuilds the initramfs once after a leaf changed it" "$(cat "$REBUILDS" 2>/dev/null)"
[[ ! -e $root/var/lib/omarchy/image/initramfs-inputs ]] || fail "the first boot clears the rebuild it owed"
grep -q 'Completed: .*/install/hardware/c.sh' "$root/var/log/omarchy-install.log" ||
fail "the first boot logs each step to the install log"
pass "the first boot runs the deferred hardware setup with no command from the owner"
rm -f "$RUNS" "$REBUILDS"
output=$(first_boot "$root") || fail "running the first-boot hardware setup again succeeds"
[[ -z $output && ! -e $RUNS && ! -e $REBUILDS ]] ||
fail "running the first-boot hardware setup again does nothing" "$output"
pass "running the first-boot hardware setup again does nothing"
# The live platform is asked once the manifest is retired, so it is the
# hardware's even where the detector cannot tell a booted root (this fixture
# runs no systemd): an Apple Silicon image booted in a VM reports plain aarch64.
fake_platform "$test_tmp/vm" aarch64
reset_logs
root=$(new_root vm)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds for a VM boot"
output=$(OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$fixture" OMARCHY_PROC_ROOT="$test_tmp/vm/proc" \
PATH="$test_tmp/vm/bin:$stub_bin:$ROOT/bin:/usr/local/bin:/usr/bin:/bin" "$ROOT/bin/omarchy-provision-hardware") ||
fail "the first boot on other hardware finishes" "$output"
[[ $output == *"First boot of an image built for aarch64-apple, on aarch64 hardware"* ]] ||
fail "the first boot reports the hardware it runs on, not the image target" "$output"
pass "the first boot reports the hardware it runs on, not the image target"
# A failed step stays queued with everything after it; the next boot resumes there.
reset_logs
root=$(new_root resume)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
# A repository without a sync database, which the image never has: the failed
# step is not retried after a fetch.
mkdir -p "$root/etc"
printf '[options]\nArchitecture = auto\n\n[core]\nServer = https://mirror.invalid/$arch/$repo\n' >"$root/etc/pacman.conf"
touch "$FAIL_B"
status=0
output=$(first_boot "$root" 2>&1) || status=$?
(( status == 75 )) || fail "a failed step exits 75, so a platform first boot can tell it from a refusal" "status $status: $output"
[[ $output == *"Deferred hardware step failed: install/hardware/apple/b.sh"* ]] ||
fail "the first boot names the failed step" "$output"
[[ $(cat "$RUNS") == "a user= path=$fixture" ]] || fail "the first boot stops at the failed step" "$(cat "$RUNS")"
[[ $(queue_of "$root") == $'install/hardware/apple/b.sh\ninstall/hardware/c.sh' ]] ||
fail "the failed step and those after it stay queued" "$(queue_of "$root")"
[[ -f $root/var/lib/omarchy/image/target.booted && ! -e $root/var/lib/omarchy/image/target ]] ||
fail "the machine stops being an image build once its first boot starts"
[[ -L $root/etc/systemd/system/multi-user.target.wants/$unit_name && -e $root/var/lib/omarchy/image/initramfs-inputs && ! -e $REBUILDS ]] ||
fail "the service stays armed and the initramfs rebuild stays owed until the queue is empty"
[[ ! -e $LEAF_CALLS ]] || fail "a failed step fetches no package databases" "$(cat "$LEAF_CALLS")"
pass "a failed step stays queued with the steps after it"
rm -f "$FAIL_B" "$RUNS"
stub_rebuild limine-mkinitcpio
output=$(first_boot "$root") || fail "the next boot finishes the deferred hardware setup" "$output"
[[ $(cat "$RUNS") == $'b\nc' ]] || fail "the next boot resumes at the failed step" "$(cat "$RUNS")"
[[ $(cat "$REBUILDS") == "limine-mkinitcpio " ]] ||
fail "the next boot rebuilds the initramfs the earlier boot owed, through Limine when present" "$(cat "$REBUILDS" 2>/dev/null)"
[[ ! -e $root/var/lib/omarchy/image/deferred-steps && ! -L $root/etc/systemd/system/multi-user.target.wants/$unit_name ]] ||
fail "the next boot disarms the service"
pass "the next boot resumes at the failed step and finishes"
# The step that changed the initramfs inputs is the one that failed: its rerun
# changes nothing more, and the rebuild is still owed.
reset_logs
rm -f "$stub_bin/limine-mkinitcpio"
root=$(new_root rebuild)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/apple/b.sh install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
touch "$FAIL_B"
first_boot "$root" >/dev/null 2>&1 && fail "the first boot reports the failed step"
rm -f "$FAIL_B"
first_boot "$root" >/dev/null || fail "the next boot finishes the deferred hardware setup"
[[ $(cat "$RUNS") == $'b\nc' && $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" ]] ||
fail "a step that changed the initramfs and then failed still gets it rebuilt" "$(cat "$REBUILDS" 2>/dev/null)"
pass "a step that changed the initramfs and then failed still gets it rebuilt"
# A step that only adds to the kernel command line Limine's entry tool builds
# (as intel/fred.sh does) gets the rebuild too.
reset_logs
root=$(new_root cmdline)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
cat >"$fixture/install/hardware/cmdline.sh" <<'SH'
mkdir -p "$OMARCHY_IMAGE_ROOT/etc/limine-entry-tool.d"
echo 'KERNEL_CMDLINE[default]+=" example=on"' >"$OMARCHY_IMAGE_ROOT/etc/limine-entry-tool.d/example.conf"
SH
printf '%s\n' install/hardware/cmdline.sh >"$root/var/lib/omarchy/image/deferred-steps"
first_boot "$root" >/dev/null || fail "a step that changes the kernel command line finishes"
[[ $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" ]] ||
fail "a step that only changed Limine's kernel command line gets the rebuild" "$(cat "$REBUILDS" 2>/dev/null)"
rm -f "$fixture/install/hardware/cmdline.sh"
pass "a step that only changed Limine's kernel command line gets the rebuild"
# Module options reach the initramfs through mkinitcpio's modconf hook, so a
# step that only writes /etc/modprobe.d (as nvidia.sh does) gets the rebuild too.
reset_logs
root=$(new_root modprobe)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
cat >"$fixture/install/hardware/modprobe.sh" <<'SH'
mkdir -p "$OMARCHY_IMAGE_ROOT/etc/modprobe.d"
echo 'options example flag=1' >"$OMARCHY_IMAGE_ROOT/etc/modprobe.d/example.conf"
SH
printf '%s\n' install/hardware/modprobe.sh >"$root/var/lib/omarchy/image/deferred-steps"
first_boot "$root" >/dev/null || fail "a step that changes module options finishes"
[[ $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" ]] ||
fail "a step that only changed module options gets the rebuild" "$(cat "$REBUILDS" 2>/dev/null)"
rm -f "$fixture/install/hardware/modprobe.sh"
pass "a step that only changed module options gets the rebuild"
# --- The pacman keyring -------------------------------------------------------
# Install finalization makes the pacman keyring on plain aarch64.
# An image build asks for it instead, so every machine flashed from the image
# makes its own master key on its first boot, before any step installs a package.
keyring_bin="$test_tmp/keyring-bin"
mkdir -p "$keyring_bin" "$fixture/install/post-install"
# Finalization's repositories are pacman-templates-test.sh's; here they land in
# the root under test, never the host's /etc.
sed 's|/etc/pacman|$OMARCHY_IMAGE_ROOT/etc/pacman|g' "$ROOT/install/post-install/pacman.sh" >"$fixture/install/post-install/pacman.sh"
cp "$ROOT/install/helpers/pacman.sh" "$fixture/install/helpers/pacman.sh"
mkdir -p "$fixture/default"
cp -r "$ROOT/default/pacman" "$fixture/default/"
: >"$fixture/install/hardware/pacman.sh"
for command in pacman-key omarchy-pkg-add; do
cat >"$keyring_bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >>"$KEYRING"
[[ ${0##*/} != "pacman-key" || ! -e $KEYRING_FAIL ]]
SH
done
printf '#!/bin/bash\necho "$PLATFORM"\n' >"$keyring_bin/omarchy-hw-platform"
chmod +x "$keyring_bin"/*
export KEYRING="$test_tmp/keyring" KEYRING_FAIL="$test_tmp/keyring-fail"
request=var/lib/omarchy/image/pacman-keyring
finalize() {
mkdir -p "$1/etc/pacman.d"
OMARCHY_IMAGE_ROOT="$1" OMARCHY_PATH="$fixture" OMARCHY_INSTALL="$fixture/install" PLATFORM=$2 \
PATH="$keyring_bin:$base_path" bash -e -c 'source "$1"' bash "$fixture/install/post-install/pacman.sh"
}
keyring_boot() {
OMARCHY_IMAGE_ROOT="$1" OMARCHY_PATH="$fixture" OMARCHY_PROC_ROOT="$test_tmp/hw/proc" \
PATH="$keyring_bin:$base_path" "$ROOT/bin/omarchy-provision-hardware"
}
for platform in aarch64; do
rm -f "$KEYRING"
root=$(new_root "install-$platform")
finalize "$root" "$platform" || fail "$platform: install finalization succeeds"
[[ $(cat "$KEYRING") == $'omarchy-pkg-add archlinuxarm-keyring\npacman-key --init\npacman-key --populate' ]] ||
fail "$platform: an install makes its pacman keyring in finalization, as before" "$(cat "$KEYRING")"
[[ ! -e $root/$request ]] || fail "$platform: an install requests no first-boot keyring"
rm -f "$KEYRING"
root=$(new_root "image-$platform")
write_manifest "$root" $'format=1\nplatform='"$platform"$'\n'
build "$root" >/dev/null || fail "$platform: the fixture image builds"
finalize "$root" "$platform" || fail "$platform: image finalization succeeds"
[[ $(cat "$KEYRING") == "omarchy-pkg-add archlinuxarm-keyring" ]] ||
fail "$platform: an image build makes no pacman keyring" "$(cat "$KEYRING")"
[[ -f $root/$request ]] || fail "$platform: an image build asks its first boot for the keyring"
done
for platform in aarch64-apple x86; do
rm -f "$KEYRING"
root=$(new_root "image-$platform")
write_manifest "$root" $'format=1\nplatform='"$platform"$'\n'
build "$root" >/dev/null || fail "$platform: the fixture image builds"
finalize "$root" "$platform" || fail "$platform: image finalization succeeds"
finalize "$(new_root "install-$platform")" "$platform" || fail "$platform: install finalization succeeds"
[[ ! -e $KEYRING && ! -e $root/$request ]] || fail "$platform: finalization leaves the keyring alone" "$(cat "$KEYRING" 2>/dev/null)"
done
pass "an image build on plain aarch64 leaves the pacman keyring to the first boot"
reset_logs
rm -f "$KEYRING"
root=$(new_root image-aarch64-apple)
write_manifest "$root"
build "$root" >/dev/null || fail "the Mac fixture image builds"
keyring_boot "$root" >/dev/null || fail "a Mac image's first boot finishes"
[[ ! -e $KEYRING ]] || fail "a first boot nobody asked for a keyring leaves the keyring alone" "$(cat "$KEYRING")"
pass "a first boot nobody asked for a keyring leaves the keyring alone"
reset_logs
root=$test_tmp/root-image-aarch64
mkdir -p "$root/etc/pacman.d/gnupg"
touch "$root/etc/pacman.d/gnupg/build-master-key" "$KEYRING_FAIL"
status=0
output=$(KEYRING=$RUNS keyring_boot "$root" 2>&1) || status=$?
(( status == 75 )) || fail "a keyring that can't be made exits 75" "status $status: $output"
[[ $output == *"could not create the pacman keyring"* ]] || fail "the first boot says the keyring failed" "$output"
[[ $(cat "$RUNS") == "pacman-key --gpgdir $root/etc/pacman.d/gnupg --init" ]] ||
fail "no step runs before the keyring exists" "$(cat "$RUNS")"
[[ -f $root/$request && $(queue_of "$root") == $'install/hardware/a.sh\ninstall/hardware/apple/b.sh\ninstall/hardware/c.sh' ]] ||
fail "the keyring request and the whole queue stay for the next boot"
pass "a keyring that can't be made stays requested and holds back every step"
rm -f "$KEYRING_FAIL" "$RUNS"
mkdir -p "$root/etc/pacman.d/gnupg"
touch "$root/etc/pacman.d/gnupg/build-master-key"
output=$(KEYRING=$RUNS keyring_boot "$root") || fail "the next boot makes the keyring and finishes" "$output"
[[ $(cat "$RUNS") == "pacman-key --gpgdir $root/etc/pacman.d/gnupg --init"$'\n'"pacman-key --gpgdir $root/etc/pacman.d/gnupg --populate"$'\na user= path='"$fixture"$'\nb\nc' ]] ||
fail "the first boot makes the keyring, then runs the steps" "$(cat "$RUNS")"
[[ ! -e $root/etc/pacman.d/gnupg/build-master-key ]] || fail "the first boot replaces a keyring the build left"
[[ ! -e $root/$request && ! -e $root/var/lib/omarchy/image/deferred-steps ]] || fail "the first boot clears the keyring request"
pass "the first boot replaces any build keyring with the machine's own before the steps run"
# A keyring request with nothing queued (a build whose hardware setup queued no
# step) still gets its keyring.
reset_logs
rm -f "$RUNS"
root=$test_tmp/root-keyring-only
mkdir -p "$root/var/lib/omarchy/image"
install -m 0644 /dev/null "$root/$request"
output=$(KEYRING=$RUNS keyring_boot "$root") || fail "a first boot with only a keyring request finishes" "$output"
[[ $(cat "$RUNS") == "pacman-key --gpgdir $root/etc/pacman.d/gnupg --init"$'\n'"pacman-key --gpgdir $root/etc/pacman.d/gnupg --populate" && ! -e $root/$request ]] ||
fail "a keyring request is honoured with no queued step" "$(cat "$RUNS" 2>/dev/null)"
grep -qx 'ConditionPathExists=|/var/lib/omarchy/image/pacman-keyring' "$ROOT/install/provisioning/omarchy-provision-hardware.service" ||
fail "the first-boot unit also starts for a keyring request alone"
pass "a keyring request with no queued step still gets the machine's own keyring"
# A leaf a later Omarchy no longer ships is dropped; a queue entry outside the
# hardware setup stops the run before anything runs.
reset_logs
root=$(new_root gone)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/removed.sh install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
output=$(first_boot "$root") || fail "a step no longer shipped does not block the rest" "$output"
[[ $output == *"Skipping deferred hardware step install/hardware/removed.sh"* && $(cat "$RUNS") == "c" ]] ||
fail "a step no longer shipped is skipped and the rest run" "$output"
pass "a deferred step Omarchy no longer ships is skipped"
# A step that would build the boot image itself asks for the rebuild after the
# last step instead: one rebuild, even when no
# initramfs input changed, and the request outlives a run that stopped early.
reset_logs
root=$(new_root request)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
first_boot "$root" >/dev/null || fail "a step that asks for nothing finishes"
[[ ! -e $REBUILDS ]] || fail "no rebuild without a changed input or a request" "$(cat "$REBUILDS")"
reset_logs
root=$(new_root request)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
touch "$REQUEST_REBUILD"
first_boot "$root" >/dev/null || fail "a step that asks for the rebuild finishes"
rm -f "$REQUEST_REBUILD"
[[ $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" && ! -e $root/var/lib/omarchy/image/boot-rebuild ]] ||
fail "the request alone, with no changed input, gets one rebuild and is then cleared" "$(cat "$REBUILDS" 2>/dev/null)"
reset_logs
root=$(new_root request)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' install/hardware/c.sh install/hardware/apple/b.sh >"$root/var/lib/omarchy/image/deferred-steps"
rm -f "$root/etc/mkinitcpio.conf.d/b.conf"
touch "$REQUEST_REBUILD" "$FAIL_B"
first_boot "$root" >/dev/null 2>&1 && fail "the first boot reports the failed step"
[[ -f $root/var/lib/omarchy/image/boot-rebuild && ! -e $REBUILDS ]] ||
fail "a requested rebuild waits for the queue to empty, and stays requested"
rm -f "$FAIL_B" "$REQUEST_REBUILD"
first_boot "$root" >/dev/null || fail "the next boot finishes the deferred hardware setup"
[[ $(cat "$REBUILDS" 2>/dev/null) == "mkinitcpio -P" && ! -e $root/var/lib/omarchy/image/boot-rebuild ]] ||
fail "the requested rebuild runs once after the last step and is then cleared" "$(cat "$REBUILDS" 2>/dev/null)"
pass "a step can ask for the one rebuild after the last step"
for entry in /etc/passwd install/hardware/../../bin/x.sh install/login/sddm.sh; do
reset_logs
root=$(new_root unsafe)
write_manifest "$root"
build "$root" >/dev/null || fail "the fixture image builds"
printf '%s\n' "$entry" install/hardware/c.sh >"$root/var/lib/omarchy/image/deferred-steps"
status=0
first_boot "$root" >/dev/null 2>&1 || status=$?
(( status == 1 )) || fail "the first boot refuses the queue entry $entry with status 1, not a step failure's 75" "status $status"
[[ ! -e $RUNS && $(head -n 1 "$root/var/lib/omarchy/image/deferred-steps") == "$entry" ]] ||
fail "the first boot refuses the queue entry $entry before running anything"
done
pass "the first boot runs only hardware setup leaves from its queue"
reset_logs
root=$(new_root nothing)
output=$(first_boot "$root") || fail "the first-boot hardware setup succeeds with nothing deferred"
[[ -z $output && ! -e $root/var/lib/omarchy ]] || fail "the first-boot hardware setup does nothing with nothing deferred" "$output"
pass "with nothing deferred the first-boot hardware setup does nothing"
if env -u OMARCHY_IMAGE_ROOT OMARCHY_PATH="$fixture" PATH="$base_path" "$ROOT/bin/omarchy-provision-hardware" >/dev/null 2>&1; then
fail "the first-boot hardware setup refuses a normal user"
fi
pass "the first-boot hardware setup refuses a normal user"
# The service fires on the queue this helper writes and hands the machine to
# owner setup and the login screen only after it.
grep -qx 'ConditionPathExists=|/var/lib/omarchy/image/deferred-steps' "$unit_source" &&
grep -qx 'ExecStart=/usr/bin/omarchy-provision-hardware' "$unit_source" &&
grep -qx 'Before=omarchy-provision-owner.service display-manager.service' "$unit_source" &&
grep -qx 'WantedBy=multi-user.target' "$unit_source" ||
fail "the first-boot hardware service runs on the queue before owner setup and the login screen"
pass "the first-boot hardware service runs on the queue before owner setup and the login screen"
# --- Bluetooth on the first boot ---------------------------------------------
# The adapter brings up bluetooth.target before the first boot runs the
# Bluetooth leaf, and a target never starts a unit enabled after its start job
# was made, so that boot has to start bluetooth.service itself. Nothing else
# does: an install or a rerun of hardware setup leaves the service as it finds it.
bt_fixture="$test_tmp/omarchy-bluetooth"
mkdir -p "$bt_fixture/install/helpers" "$bt_fixture/install/provisioning" "$bt_fixture/install/hardware"
cp "$ROOT/install/helpers/logging.sh" "$ROOT/install/helpers/image-target.sh" "$bt_fixture/install/helpers/"
cp "$unit_source" "$bt_fixture/install/provisioning/"
cp "$ROOT/install/hardware/bluetooth.sh" "$bt_fixture/install/hardware/"
cp "$fixture/install/hardware/c.sh" "$bt_fixture/install/hardware/"
printf 'run_logged "$OMARCHY_INSTALL/hardware/%s"\n' bluetooth.sh c.sh >"$bt_fixture/install/hardware/all.sh"
bt_bin="$test_tmp/bt-bin"
mkdir -p "$bt_bin"
cat >"$bt_bin/systemctl" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$SYSTEMCTL_CALLS"
case $* in
"start --no-block bluetooth.service") [[ ! -e $BT_START_REFUSED ]] ;;
*) exit 0 ;;
esac
SH
chmod +x "$bt_bin/systemctl"
export SYSTEMCTL_CALLS="$test_tmp/systemctl-calls" BT_START_REFUSED="$test_tmp/bt-start-refused"
bt_path="$bt_bin:$base_path"
bt_image() {
local root
root=$(new_root "bt-$1")
write_manifest "$root"
OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$bt_fixture" OMARCHY_INSTALL_LOG_FILE="$root/var/log/omarchy-install.log" \
PATH="$bt_path" "$apply_hardware" --defer-provisioning >/dev/null || fail "the Bluetooth fixture image builds"
printf '%s\n' "$root"
}
bt_first_boot() {
rm -f "$SYSTEMCTL_CALLS" "$RUNS"
OMARCHY_IMAGE_ROOT="$1" OMARCHY_PATH="$bt_fixture" OMARCHY_PROC_ROOT="$test_tmp/hw/proc" \
PATH="$bt_path" "$ROOT/bin/omarchy-provision-hardware" >/dev/null 2>&1
}
started=$'enable bluetooth.service\nstart --no-block bluetooth.service'
reset_logs
rm -f "$BT_START_REFUSED" "$SYSTEMCTL_CALLS"
root=$(bt_image started)
[[ ! -e $SYSTEMCTL_CALLS ]] || fail "an image build touches no Bluetooth unit" "$(cat "$SYSTEMCTL_CALLS")"
bt_first_boot "$root" || fail "the first boot finishes the Bluetooth leaf"
[[ $(<"$SYSTEMCTL_CALLS") == "$started" ]] ||
fail "the first boot enables Bluetooth and starts it without waiting for the job" "$(<"$SYSTEMCTL_CALLS")"
[[ $(cat "$RUNS") == c && ! -e $root/var/lib/omarchy/image/deferred-steps ]] ||
fail "the first boot goes on past the Bluetooth leaf"
pass "the first boot enables Bluetooth and starts it in the same session"
touch "$BT_START_REFUSED"
root=$(bt_image refused)
bt_first_boot "$root" || fail "a refused Bluetooth start does not fail the first boot"
[[ $(<"$SYSTEMCTL_CALLS") == "$started" && $(cat "$RUNS") == c && ! -e $root/var/lib/omarchy/image/deferred-steps ]] ||
fail "a refused Bluetooth start leaves the step done and the queue going" "$(<"$SYSTEMCTL_CALLS")"
pass "a refused Bluetooth start does not fail the first boot"
rm -f "$BT_START_REFUSED"
# A later boot that retries the queue is still the machine's own first setup.
root=$(bt_image retry)
mv "$root/var/lib/omarchy/image/target" "$root/var/lib/omarchy/image/target.booted"
bt_first_boot "$root" || fail "a retried queue finishes"
[[ $(<"$SYSTEMCTL_CALLS") == "$started" ]] ||
fail "a retried queue starts Bluetooth like the first run" "$(<"$SYSTEMCTL_CALLS")"
pass "a retried queue starts Bluetooth like the first run"
# Hardware setup outside the first boot, even with the marker inherited.
rm -f "$SYSTEMCTL_CALLS" "$RUNS"
root=$(new_root bt-live)
OMARCHY_IMAGE_DEFERRED_HARDWARE=1 OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$bt_fixture" \
OMARCHY_INSTALL_LOG_FILE="$root/var/log/omarchy-install.log" PATH="$bt_path" "$apply_hardware" --defer-provisioning >/dev/null ||
fail "hardware setup without a manifest runs the Bluetooth leaf"
[[ $(<"$SYSTEMCTL_CALLS") == "enable bluetooth.service" && $(cat "$RUNS") == c ]] ||
fail "hardware setup outside the first boot only enables Bluetooth" "$(<"$SYSTEMCTL_CALLS")"
pass "hardware setup outside the first boot only enables Bluetooth, as an install always has"
# --- Root ignores the fixture root ------------------------------------------
# As namespaced root the fixture root in the environment is ignored: the
# fixture image is not built into, and hardware setup runs as on any live system.
if [[ -e /var/lib/omarchy/image ]]; then
pass "this machine keeps image state of its own; skipping the root override probe"
elif unshare --user --map-root-user true 2>/dev/null; then
root_runner=(unshare --user --map-root-user)
reset_logs
root=$(new_root root-build)
write_manifest "$root"
OMARCHY_IMAGE_ROOT="$root" OMARCHY_PATH="$fixture" OMARCHY_INSTALL_LOG_FILE="$root/var/log/omarchy-install.log" \
PATH="$base_path" "${root_runner[@]}" "$ROOT/bin/omarchy-apply-hardware" --defer-provisioning >/dev/null ||
fail "root runs hardware setup with a fixture root in its environment"
[[ $(cat "$RUNS") == $'a user= path='"$fixture"$'\nb\nc' && ! -e $root/var/lib/omarchy/image/deferred-steps ]] ||
fail "root ignores a fixture manifest named by its environment"
# Both commands take every path from the helper, which as root answers with
# the live ones whatever the environment says.
resolved=$(OMARCHY_IMAGE_ROOT="$root" "${root_runner[@]}" bash -c \
'source "$1"; omarchy_image_init && printf "%s\n" "$omarchy_image_manifest" "$omarchy_image_queue" "$omarchy_image_systemd_dir"' \
bash "$ROOT/install/helpers/image-target.sh") || fail "root resolves the image paths"
[[ $resolved == $'/var/lib/omarchy/image/target\n/var/lib/omarchy/image/deferred-steps\n/etc/systemd/system' ]] ||
fail "root resolves the live image paths whatever its environment names" "$resolved"
pass "root ignores fixture roots in its environment"
else
pass "no unprivileged user namespace; skipping the root override probe"
fi
+442
View File
@@ -0,0 +1,442 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# omarchy-hw-platform answers with the image-target manifest while a root is
# being built, and with the hardware once it boots. Each world below is what one
# detector run sees: image/ is the root, proc/ its /proc (the host's device tree
# and PID 1), bin/ a uname for the CPU it runs on.
detector="$ROOT/bin/omarchy-hw-platform"
umask 022
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
apple_manifest=$'format=1\nplatform=aarch64-apple\n'
# $1 world, $2 CPU, then the host's device-tree tokens (none: no device tree).
world() {
local dir="$test_tmp/worlds/$1" cpu=$2
shift 2
rm -rf "$dir"
mkdir -p "$dir/image" "$dir/proc" "$dir/bin" "$dir/host"
if (( $# )); then
mkdir -p "$dir/proc/device-tree"
printf '%s\0' "$@" >"$dir/proc/device-tree/compatible"
fi
cat >"$dir/bin/uname" <<SH
#!/bin/bash
[[ \${1:-} == -m ]] && { echo $cpu; exit 0; }
exec /usr/bin/uname "\$@"
SH
chmod +x "$dir/bin/uname"
}
# $1 world, $2 how PID 1 sees the root: chroot (PID 1 is the build host's),
# own (PID 1 runs in the root, as in a PID namespace or a booted system),
# hidden (PID 1 is visible but its root is not, as for a normal user); $3 what
# PID 1 is, systemd unless named. Without a call the world has no /proc/1, as
# in a root without /proc or behind hidepid.
pid1() {
local dir="$test_tmp/worlds/$1"
mkdir -p "$dir/proc/1"
echo "${3:-systemd}" >"$dir/proc/1/comm"
case $2 in
chroot) ln -s ../../host "$dir/proc/1/root" ;;
own) ln -s ../../image "$dir/proc/1/root" ;;
hidden) ;;
esac
}
systemd_runs() {
mkdir -p "$test_tmp/worlds/$1/image/run/systemd/system"
}
# $1 world, $2 manifest body.
manifest() {
local image_dir="$test_tmp/worlds/$1/image/var/lib/omarchy/image"
mkdir -p "$image_dir"
chmod 0755 "$image_dir"
printf '%s' "$2" >"$image_dir/target"
chmod 0644 "$image_dir/target"
}
in_world() {
local dir="$test_tmp/worlds/$1"
shift
OMARCHY_PROC_ROOT="$dir/proc" OMARCHY_IMAGE_ROOT="$dir/image" PATH="$dir/bin:$ROOT/bin:$PATH" "$@"
}
detect() {
in_world "$1" "$detector" 2>"$test_tmp/error"
}
expect() {
local name=$1 expected=$2 description=$3 actual
actual=$(detect "$name") || fail "$description" "$(cat "$test_tmp/error")"
[[ $actual == "$expected" ]] || fail "$description" "expected: $expected
actual: $actual"
pass "$description"
}
expect_refused() {
local name=$1 message=$2 description=$3
if detect "$name" >/dev/null; then
fail "$description"
fi
grep -Fq "$message" "$test_tmp/error" || fail "$description explains itself" "$(cat "$test_tmp/error")"
pass "$description"
}
# --- Root reads only the live system -----------------------------------------
root_runner=()
if (( EUID != 0 )); then
root_runner=(unshare --user --map-root-user)
fi
if (( EUID == 0 )) || unshare --user --map-root-user true 2>/dev/null; then
live=$("${root_runner[@]}" "$detector") || fail "root detects the live platform"
apple_live=0
"${root_runner[@]}" "$ROOT/bin/omarchy-hw-aarch64-apple" || apple_live=$?
# A world that would make any environment-led detector answer with another
# platform than the live one.
if [[ $live == "aarch64-apple" ]]; then
hostile=aarch64
hostile_tokens=("raspberrypi,5-model-b" "brcm,bcm2712")
else
hostile=aarch64-apple
hostile_tokens=("apple,j416c" "apple,t6021" "apple,arm-platform")
fi
world hostile aarch64 "${hostile_tokens[@]}"
pid1 hostile chroot
manifest hostile $'format=1\nplatform='"$hostile"$'\n'
hostile_dir="$test_tmp/worlds/hostile"
printf 'echo %s\nexit 0\n' "$hostile" >"$test_tmp/bash-env"
# Exported functions shadow commands and even builtins in a plain bash, and
# BASH_ENV runs before its first line; --clean-environment is what the
# detector passes itself once it has restarted.
hostile_env=(
OMARCHY_PROC_ROOT="$hostile_dir/proc" OMARCHY_SYS_ROOT="$hostile_dir/sys" OMARCHY_IMAGE_ROOT="$hostile_dir/image"
PATH="$hostile_dir/bin:$ROOT/bin:$PATH" BASH_ENV="$test_tmp/bash-env"
'BASH_FUNC_uname%%=() { echo aarch64; }'
'BASH_FUNC_mapfile%%=() { tokens=('"${hostile_tokens[0]}"'); }'
'BASH_FUNC_stat%%=() { echo 0 644; }'
'BASH_FUNC_dirname%%=() { echo '"$hostile_dir/bin"'; }'
)
printf '#!/bin/bash\necho %s\n' "$hostile" >"$hostile_dir/bin/omarchy-hw-platform"
chmod +x "$hostile_dir/bin/omarchy-hw-platform"
for flag in "" --clean-environment; do
args=()
[[ -z $flag ]] || args=("$flag")
overridden=$(env "${hostile_env[@]}" "${root_runner[@]}" "$detector" "${args[@]}") ||
fail "root detects the live platform with a hostile environment${flag:+ and $flag}"
[[ $overridden == "$live" ]] ||
fail "root ignores fixture roots, PATH, BASH_ENV and exported functions${flag:+ with $flag}" "live: $live
hostile: $overridden"
done
apple_hostile=0
env "${hostile_env[@]}" "${root_runner[@]}" "$ROOT/bin/omarchy-hw-aarch64-apple" || apple_hostile=$?
(( apple_hostile == apple_live )) || fail "the Apple predicate ignores a hostile root environment"
pass "root ignores fixture roots, PATH, BASH_ENV and exported functions"
else
skip "no unprivileged user namespace; skipping the root environment probe"
fi
# Root with a manifest at the live path: a private mount namespace lays a
# fixture over /var/lib and /run, and in a PID namespace PID 1 is a copy of bash
# named for what it plays, the build or systemd, and runs the detector.
mkdir -p "$test_tmp/init"
cp "$BASH" "$test_tmp/init/bash"
cp "$BASH" "$test_tmp/init/systemd"
if (( EUID != 0 )) && "$test_tmp/init/bash" -c true 2>/dev/null &&
unshare --user --map-root-user --mount --pid --fork --mount-proc true 2>/dev/null; then
# $1 fixture, $2 PID 1 (bash or systemd), $3 a file to lay over the manifest.
live_root() {
local fixture=$1 init=$2 owner_file=${3:-}
unshare --user --map-root-user --mount --pid --fork --mount-proc "$test_tmp/init/$init" -c '
mount --bind "$1/var/lib" /var/lib && mount --bind "$1/run" /run || exit 99
if [[ -n $3 ]]; then mount --bind "$3" /var/lib/omarchy/image/target || exit 99; fi
"$2"
status=$?
exit "$status"
' "$init" "$fixture" "$detector" "$owner_file" 2>"$test_tmp/error"
}
baseline=$(unshare --user --map-root-user --mount --pid --fork --mount-proc "$detector") ||
fail "root detects the live platform in a PID namespace"
# A target the hardware would not give, so only the manifest can name it.
target=aarch64-apple
[[ $baseline != "aarch64-apple" ]] || target=aarch64
fixture="$test_tmp/live-root"
mkdir -p "$fixture/var/lib/omarchy/image" "$fixture/run"
printf 'format=1\nplatform=%s\n' "$target" >"$fixture/var/lib/omarchy/image/target"
expect_live_manifest() {
local description=$1 actual
if [[ $(uname -m) == aarch64 ]]; then
actual=$(live_root "$fixture" bash) || fail "$description" "$(cat "$test_tmp/error")"
[[ $actual == "$target" ]] || fail "$description" "expected: $target
actual: $actual"
else
if live_root "$fixture" bash >/dev/null; then
fail "$description, and a $target target on $(uname -m) contradicts it"
fi
grep -Fq "/var/lib/omarchy/image/target names $target hardware but the CPU is" "$test_tmp/error" ||
fail "$description" "$(cat "$test_tmp/error")"
fi
pass "$description"
}
expect_live_manifest "root answers with the live manifest in a build with a /run of its own"
mkdir -p "$fixture/run/systemd/system"
expect_live_manifest "root answers with the live manifest in a build whose PID 1 is not systemd, with systemd in /run"
actual=$(live_root "$fixture" systemd) || fail "a booted root ignores the live manifest" "$(cat "$test_tmp/error")"
[[ $actual == "$baseline" ]] || fail "a booted root ignores the live manifest" "expected: $baseline
actual: $actual"
pass "a booted root answers with its hardware despite a manifest"
rmdir "$fixture/run/systemd/system"
# A file the namespace's root does not own: the real root's, unmapped here.
if live_root "$fixture" bash /etc/os-release >/dev/null; then
fail "root refuses a manifest another user owns"
fi
grep -Fq "is not a root-owned regular file" "$test_tmp/error" || fail "root refuses a manifest another user owns" "$(cat "$test_tmp/error")"
pass "root refuses a manifest another user owns"
else
skip "no unprivileged user, mount and PID namespaces, or an executable temporary directory; skipping the live manifest probe"
fi
require_platform_fixtures "the image-target fixtures"
# --- Image builds -------------------------------------------------------------
# An Apple image built on an x86 host: the chroot runs under aarch64 emulation,
# the host has no device tree, and PID 1 is the host's.
world x86-host aarch64
pid1 x86-host chroot
manifest x86-host "$apple_manifest"
expect x86-host aarch64-apple "an Apple image built in a chroot on an x86 host is Apple Silicon"
# The same with the host's /run bound into the chroot, systemd's directory and all.
systemd_runs x86-host
expect x86-host aarch64-apple "a chroot build ignores the host's systemd in a bound /run"
# On a plain aarch64 host, whose own device tree the chroot can see.
world arm-host aarch64 linux,dummy-virt
pid1 arm-host chroot
manifest arm-host "$apple_manifest"
expect arm-host aarch64-apple "an Apple image built in a chroot on a plain aarch64 host is Apple Silicon"
# The image builder's isolated chroot: a PID namespace whose PID 1 is the build.
world builder aarch64 raspberrypi,5-model-b brcm,bcm2712
pid1 builder own bash
manifest builder "$apple_manifest"
expect builder aarch64-apple "an Apple image built in its own PID namespace is Apple Silicon"
# The same with the host's /run bound in: PID 1 is still not systemd.
systemd_runs builder
expect builder aarch64-apple "a PID namespace build ignores the host's systemd in a bound /run"
# A root without /proc, with a /run of its own.
world no-proc aarch64
manifest no-proc "$apple_manifest"
expect no-proc aarch64-apple "an Apple image built without /proc is Apple Silicon"
# The host never decides the target, even when it is a Mac or its tree is
# contradictory.
world on-a-mac aarch64 apple,j416c apple,t6021 apple,arm-platform
pid1 on-a-mac chroot
manifest on-a-mac $'format=1\nplatform=aarch64\n'
expect on-a-mac aarch64 "a plain aarch64 image built on a Mac is plain aarch64"
world odd-host aarch64 apple,j416c qcom,x1e80100
pid1 odd-host chroot
manifest odd-host $'format=1\nplatform=aarch64\n'
expect odd-host aarch64 "a build never reads the host's device tree"
# Image builders written before the platform names settled write the old ones.
world legacy-apple aarch64
pid1 legacy-apple chroot
manifest legacy-apple $'format=1\nplatform=apple-silicon\n'
expect legacy-apple aarch64-apple "an old builder's apple-silicon manifest reads as aarch64-apple"
world legacy-arm aarch64
pid1 legacy-arm chroot
manifest legacy-arm $'format=1\nplatform=generic-aarch64\n'
expect legacy-arm aarch64 "an old builder's generic-aarch64 manifest reads as aarch64"
world legacy-x86 x86_64
pid1 legacy-x86 chroot
manifest legacy-x86 $'format=1\nplatform=generic\n'
expect legacy-x86 x86 "an old builder's generic manifest reads as x86"
# Comments and keys a later format adds are ignored.
world commented aarch64
pid1 commented chroot
manifest commented $'# written by the image builder\nformat=1\nbuilder=ci\nplatform=aarch64-apple\n'
expect commented aarch64-apple "comments and unknown manifest keys are ignored"
# --- Booted systems -----------------------------------------------------------
# A booted Mac whose manifest outlived its first boot: the hardware decides, and
# not even a broken manifest stops it.
world booted-mac aarch64 apple,j416c apple,t6021 apple,arm-platform
pid1 booted-mac own
systemd_runs booted-mac
manifest booted-mac $'format=1\nplatform=aarch64\n'
expect booted-mac aarch64-apple "a booted Mac with a stale manifest is Apple Silicon"
manifest booted-mac "not a manifest"
expect booted-mac aarch64-apple "a booted Mac ignores a malformed stale manifest"
world booted-qualcomm aarch64 lenovo,yoga-slim7x qcom,x1e80100
pid1 booted-qualcomm own
systemd_runs booted-qualcomm
manifest booted-qualcomm "$apple_manifest"
expect booted-qualcomm aarch64 "a booted Snapdragon laptop with an Apple manifest is plain aarch64"
world booted-x86 x86_64
pid1 booted-x86 own
systemd_runs booted-x86
manifest booted-x86 "$apple_manifest"
expect booted-x86 x86 "a booted x86 machine with an Apple manifest is x86"
# An Apple image under VM acceptance, booted before its first-boot setup ran.
world booted-vm aarch64 linux,dummy-virt
pid1 booted-vm own
systemd_runs booted-vm
manifest booted-vm "$apple_manifest"
expect booted-vm aarch64 "a booted VM with an Apple manifest is plain aarch64"
# What a normal user cannot see never makes a build: PID 1's root, or PID 1
# itself behind hidepid.
world user-view aarch64 apple,j416c apple,t6021 apple,arm-platform
pid1 user-view hidden
systemd_runs user-view
manifest user-view $'format=1\nplatform=aarch64\n'
expect user-view aarch64-apple "a booted system whose PID 1 root cannot be compared uses its hardware"
world hidepid aarch64 apple,j416c apple,t6021 apple,arm-platform
systemd_runs hidepid
manifest hidepid "not a manifest"
expect hidepid aarch64-apple "a booted system whose PID 1 is hidden uses its hardware"
# --- No manifest --------------------------------------------------------------
# An installer on the target machine chroots into the new root with no
# manifest: the hardware it runs on is the target.
world installer aarch64 apple,j314s apple,t6000 apple,arm-platform
pid1 installer chroot
expect installer aarch64-apple "a chroot without a manifest uses the hardware"
# A manifest retired by the first boot no longer names anything.
world retired aarch64 linux,dummy-virt
pid1 retired chroot
manifest retired "$apple_manifest"
mv "$test_tmp/worlds/retired/image/var/lib/omarchy/image/target" "$test_tmp/worlds/retired/image/var/lib/omarchy/image/target.booted"
expect retired aarch64 "a retired manifest is not read"
# --- Invalid manifests stop a build -------------------------------------------
invalid() {
local name=$1 body=$2 message=$3 description=$4
world "$name" aarch64 linux,dummy-virt
pid1 "$name" chroot
manifest "$name" "$body"
expect_refused "$name" "$message" "$description"
}
invalid no-format $'platform=aarch64-apple\n' "is not format=1" "a manifest without a format is refused"
invalid format-2 $'format=2\nplatform=aarch64-apple\n' "is not format=1" "a manifest of another format is refused"
invalid empty "" "is not format=1" "an empty manifest is refused"
invalid unknown $'format=1\nplatform=intel-mac\n' "names no known platform: intel-mac" "a manifest naming an unknown platform is refused"
invalid no-platform $'format=1\n' "names no known platform: none" "a manifest naming no platform is refused"
invalid bare-line $'format=1\naarch64-apple\n' "is malformed: aarch64-apple" "a manifest line without a key is refused"
world x86-cpu x86_64
pid1 x86-cpu chroot
manifest x86-cpu "$apple_manifest"
expect_refused x86-cpu "names aarch64-apple hardware but the CPU is x86_64" "an Apple manifest on an x86 CPU contradicts it"
world arm-generic aarch64
pid1 arm-generic chroot
manifest arm-generic $'format=1\nplatform=x86\n'
expect_refused arm-generic "names x86 hardware but the CPU is aarch64" "an x86 manifest on an aarch64 CPU contradicts it"
untrusted="is not a root-owned regular file"
world symlink aarch64
pid1 symlink chroot
manifest symlink "$apple_manifest"
image_dir="$test_tmp/worlds/symlink/image/var/lib/omarchy/image"
mv "$image_dir/target" "$image_dir/real-target"
ln -s real-target "$image_dir/target"
expect_refused symlink "$untrusted" "a symlinked manifest is refused"
world dangling aarch64
pid1 dangling chroot
mkdir -p "$test_tmp/worlds/dangling/image/var/lib/omarchy/image"
ln -s missing "$test_tmp/worlds/dangling/image/var/lib/omarchy/image/target"
expect_refused dangling "$untrusted" "a dangling manifest symlink is refused"
world directory aarch64
pid1 directory chroot
mkdir -p "$test_tmp/worlds/directory/image/var/lib/omarchy/image/target"
expect_refused directory "$untrusted" "a manifest that is a directory is refused"
world writable aarch64
pid1 writable chroot
manifest writable "$apple_manifest"
chmod 0664 "$test_tmp/worlds/writable/image/var/lib/omarchy/image/target"
expect_refused writable "$untrusted" "a group-writable manifest is refused"
world writable-dir aarch64
pid1 writable-dir chroot
manifest writable-dir "$apple_manifest"
chmod 0777 "$test_tmp/worlds/writable-dir/image/var/lib/omarchy/image"
expect_refused writable-dir "$untrusted" "a manifest in a world-writable directory is refused"
# Another owner: a fixture is trusted when the test's own user owns it, so a
# stat reporting root stands in for a file this user did not write.
world other-owner aarch64
pid1 other-owner chroot
manifest other-owner "$apple_manifest"
real_stat=$(command -v stat)
cat >"$test_tmp/worlds/other-owner/bin/stat" <<SH
#!/bin/bash
[[ \${!#} == */var/lib/omarchy/image/target ]] && { echo "0 644"; exit 0; }
exec $real_stat "\$@"
SH
chmod +x "$test_tmp/worlds/other-owner/bin/stat"
expect_refused other-owner "$untrusted" "a manifest another user owns is refused"
# --- What a build sets up from it ---------------------------------------------
# The Apple predicate of an Apple image built on an x86 host, and of a generic
# image built on a Mac.
in_world x86-host "$ROOT/bin/omarchy-hw-aarch64-apple" || fail "the Apple predicate accepts an Apple image build"
if in_world on-a-mac "$ROOT/bin/omarchy-hw-aarch64-apple"; then
fail "the Apple predicate rejects a plain aarch64 image built on a Mac"
fi
pass "the Apple predicate follows the image target"
# omarchy-mac's packages and units still call the predicate by its old name.
in_world x86-host "$ROOT/bin/omarchy-hw-apple-silicon" || fail "the old Apple predicate name accepts an Apple image build"
if in_world on-a-mac "$ROOT/bin/omarchy-hw-apple-silicon"; then
fail "the old Apple predicate name rejects a plain aarch64 image built on a Mac"
fi
pass "the old Apple predicate name answers as omarchy-hw-aarch64-apple"
packages=$(in_world x86-host env OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults") || fail "an Apple image build composes its packages"
[[ $packages == "$(OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults" aarch64-apple)" ]] ||
fail "an Apple image build installs the Apple Silicon package set"
packages=$(in_world on-a-mac env OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults") || fail "a plain aarch64 image build composes its packages"
[[ $packages == "$(OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults" aarch64)" ]] ||
fail "a plain aarch64 image built on a Mac installs the plain aarch64 package set"
packages=$(in_world odd-host env OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults") || fail "an image built on a contradictory host composes its packages"
[[ $packages == "$(OMARCHY_PATH="$ROOT" "$ROOT/bin/omarchy-pkg-defaults" aarch64)" ]] ||
fail "an image built on a contradictory host installs its target's package set"
pass "the default package set follows the image target"
# A caller in a private PID namespace looks like a build, so no unit may use one.
units=$(grep -rlE '^[[:space:]]*PrivatePIDs=' "$ROOT" --include='*.service' --include='*.conf' --exclude-dir=.git || true)
[[ -z $units ]] || fail "no unit runs in a private PID namespace" "$units"
pass "no unit runs in a private PID namespace"
+16 -1
View File
@@ -14,6 +14,10 @@ stub_bin="$tmpdir/bin"
mkdir -p "$home/.config" "$stub_bin"
cp -r "$ROOT/config/hypr" "$home/.config/hypr"
# No platform package: neither its key names nor its binds.
menu="$tmpdir/omarchy-menu-keybindings"
platform_root_copy "$ROOT/bin/omarchy-menu-keybindings" "$menu" "$tmpdir/no-platform"
# The menu reads binds from Hyprland, which is not running here, so stand in for
# it. A Lua bind reports dispatcher __lua and no arg, and the menu recovers both
# from the Lua source; an exec bind carries its own command. Both shapes matter:
@@ -43,7 +47,7 @@ stub_hyprctl() {
keybindings() {
env -i PATH="$stub_bin:$ROOT/bin:$PATH" HOME="$home" \
XDG_CACHE_HOME="$tmpdir/cache" OMARCHY_PATH="$ROOT" \
bash "$ROOT/bin/omarchy-menu-keybindings" --print
bash "$menu" --print
}
# Closing a window and toggling the scratchpad are two of the actions Omarchy
@@ -69,6 +73,17 @@ grep -q 'SUPER + I *→ Switch input language' <<<"$rendered" ||
fail "the input switch shortcut appears in the cheatsheet" "$rendered"
pass "the input switch shortcut appears in the cheatsheet"
# The menu's own scan of the config runs lua -E, so no LUA_INIT or LUA_PATH from
# the environment reaches it and nothing there can move the platform root.
printf "io.open('%s', 'w'):close()\n" "$tmpdir/init-ran" >"$tmpdir/init.lua"
LUA_INIT="@$tmpdir/init.lua" lua -e '' && [[ -e $tmpdir/init-ran ]] || fail "the LUA_INIT probe runs in a plain lua"
rm -f "$tmpdir/init-ran"
env -i PATH="$stub_bin:$ROOT/bin:$PATH" HOME="$home" XDG_CACHE_HOME="$tmpdir/cache-init" OMARCHY_PATH="$ROOT" \
LUA_INIT="@$tmpdir/init.lua" LUA_INIT_5_5="@$tmpdir/init.lua" LUA_INIT_5_4="@$tmpdir/init.lua" LUA_PATH="$tmpdir/?.lua" \
bash "$ROOT/bin/omarchy-menu-keybindings" --print >/dev/null
[[ ! -e $tmpdir/init-ran ]] || fail "the menu's config scan ignores LUA_INIT from the environment"
pass "the menu's config scan ignores LUA_INIT and LUA_PATH from the environment"
(( $(grep -c '→ Close window$' <<<"$rendered") == 1 )) ||
fail "an alternative chord joins the row of the first one" "$rendered"
grep -q 'SUPER + W / SUPER + Q *→ Close window' <<<"$rendered" ||
+398
View File
@@ -0,0 +1,398 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# The watcher half opens its event stream with {fd}> redirection.
(( BASH_VERSINFO[0] * 100 + BASH_VERSINFO[1] >= 401 )) || fail "this test runs under bash 4.1 or newer" "$BASH_VERSION"
lid_inhibit="$ROOT/bin/omarchy-system-lid-inhibit"
test_tmp=$(mktemp -d)
watch_pid=""
events_fd=""
# The watcher forks clamshell retries and a poll; orphaning them would leave
# them syncing against a deleted test tree.
stop_watcher() {
local child descendants
if [[ -n $watch_pid ]]; then
descendants=$(pgrep -P "$watch_pid" 2>/dev/null || true)
for child in $descendants; do
descendants+=" $(pgrep -P "$child" 2>/dev/null || true)"
done
kill -KILL "$watch_pid" 2>/dev/null || true
wait "$watch_pid" 2>/dev/null || true
for child in $descendants; do
kill -KILL "$child" 2>/dev/null || true
done
watch_pid=""
fi
if [[ -n $events_fd ]]; then
exec {events_fd}>&-
events_fd=""
fi
return 0
}
cleanup() {
stop_watcher
rm -rf "$test_tmp"
}
trap cleanup EXIT
fake_bin="$test_tmp/bin"
drm_path="$test_tmp/drm"
unit_state="$test_tmp/unit-active"
call_log="$test_tmp/calls"
monitors="$test_tmp/monitors.json"
hyprctl_fail="$test_tmp/hyprctl-fails"
busctl_fail="$test_tmp/busctl-fails"
cgroup="$test_tmp/cgroup"
events="$test_tmp/events"
mkdir -p "$fake_bin"
# The user manager, reduced to the one unit. Like the real one, it refuses to
# start a unit name that is already loaded.
cat >"$fake_bin/systemd-run" <<'SH'
#!/bin/bash
if [[ -e $OMARCHY_TEST_UNIT_STATE ]]; then
printf 'refused\n' >>"$OMARCHY_TEST_CALL_LOG"
echo "Unit already loaded" >&2
exit 1
fi
printf 'start %s\n' "$*" >>"$OMARCHY_TEST_CALL_LOG"
touch "$OMARCHY_TEST_UNIT_STATE"
SH
cat >"$fake_bin/systemctl" <<'SH'
#!/bin/bash
case "$*" in
"--user is-active --quiet omarchy-lid-inhibit.service")
[[ -e $OMARCHY_TEST_UNIT_STATE ]]
;;
"--user stop omarchy-lid-inhibit.service")
printf 'stop\n' >>"$OMARCHY_TEST_CALL_LOG"
rm -f "$OMARCHY_TEST_UNIT_STATE"
;;
*)
exit 2
;;
esac
SH
cat >"$fake_bin/busctl" <<'SH'
#!/bin/bash
[[ $* == *" HandleLidSwitchDocked" ]] || exit 1
[[ -f $OMARCHY_TEST_BUSCTL_FAIL ]] && exit 1
printf 's "%s"\n' "$OMARCHY_TEST_DOCKED_ACTION"
SH
cat >"$fake_bin/omarchy-hw-laptop" <<'SH'
#!/bin/bash
[[ $OMARCHY_TEST_LAPTOP == 1 ]]
SH
cat >"$fake_bin/hyprctl" <<'SH'
#!/bin/bash
[[ $* == "monitors all -j" ]] || exit 0
[[ -f $OMARCHY_TEST_HYPRCTL_FAIL ]] && exit 1
cat "$OMARCHY_TEST_MONITORS"
SH
chmod +x "$fake_bin"/*
export OMARCHY_DRM_PATH="$drm_path"
export OMARCHY_TEST_UNIT_STATE="$unit_state"
export OMARCHY_TEST_CALL_LOG="$call_log"
export OMARCHY_TEST_MONITORS="$monitors"
export OMARCHY_TEST_HYPRCTL_FAIL="$hyprctl_fail"
export OMARCHY_TEST_BUSCTL_FAIL="$busctl_fail"
export OMARCHY_CGROUP_PATH="$cgroup"
export OMARCHY_TEST_DOCKED_ACTION=ignore
export OMARCHY_TEST_LAPTOP=1
# Arguments come in pairs: connector and status. The card itself sits beside
# its connectors, as in sysfs.
write_connectors() {
rm -rf "$drm_path"
mkdir -p "$drm_path/card2"
while (( $# )); do
mkdir -p "$drm_path/$1"
printf '%s\n' "$2" >"$drm_path/$1/status"
shift 2
done
}
# What Hyprland reports: every output it knows, with whether it is disabled.
write_monitors() {
printf '%s\n' "$1" >"$monitors"
}
reset_unit() {
rm -f "$unit_state" "$hyprctl_fail" "$busctl_fail"
: >"$call_log"
}
# Where the caller runs: an SSH login's session scope, outside the user manager.
printf '0::/user.slice/user-1000.slice/session-3.scope\n' >"$cgroup"
clamshell='[{"name":"eDP-1","disabled":true,"dpmsStatus":true},{"name":"USB-2","disabled":false,"dpmsStatus":true}]'
sync_inhibit() {
PATH="$fake_bin:$PATH" "$lid_inhibit"
}
held() {
[[ -e $unit_state ]]
}
count_calls() {
grep -c "^$1" "$call_log" || true
}
# Apple Silicon in clamshell: the panel on eDP, a USB-C display on a "USB"
# connector that logind does not count as external.
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors "$clamshell"
sync_inhibit
held || fail "a USB-C display on a USB connector takes the lid inhibitor"
start=$(grep '^start' "$call_log")
for arg in --no-ask-password --what=handle-lid-switch --mode=block PartOf=graphical-session.target "sleep infinity"; do
[[ $start == *"$arg"* ]] || fail "the inhibitor unit is started with $arg" "$start"
done
pass "a USB-C display on a USB connector takes a blocking lid inhibitor without prompting"
[[ $start != *BindsTo=* ]] || fail "a caller outside the user manager binds the inhibitor to nothing" "$start"
pass "a caller outside the user manager binds the inhibitor to nothing"
# The watcher runs in a scope of the user manager; the inhibitor stops with it.
reset_unit
watcher_scope='app-Hyprland-omarchy\x2dhyprland\x2dmonitor\x2dwatch-f1fda151.scope'
printf '0::/user.slice/user-1000.slice/user@1000.service/app.slice/app-graphical.slice/%s\n' "$watcher_scope" >"$cgroup"
sync_inhibit
start=$(grep '^start' "$call_log")
# systemd-run unquotes property values: a single backslash would name a unit
# that does not exist, and the inhibitor would never start.
bound_scope='app-Hyprland-omarchy\\x2dhyprland\\x2dmonitor\\x2dwatch-f1fda151.scope'
[[ $start == *"--property=BindsTo=$bound_scope --property=After=$bound_scope "* ]] ||
fail "the inhibitor is bound to the watcher's unit" "$start"
printf '0::/user.slice/user-1000.slice/session-3.scope\n' >"$cgroup"
pass "the inhibitor is bound to the watcher's unit, so it cannot outlive it"
sync_inhibit
sync_inhibit
(( $(count_calls start) == 1 && $(count_calls stop) == 0 && $(count_calls refused) == 0 )) ||
fail "repeated syncs keep a single inhibitor" "$(<"$call_log")"
pass "repeated syncs keep a single inhibitor"
# Some drivers report a connector disabled while DPMS blanks it; Hyprland keeps
# the monitor enabled. Releasing here would suspend a closed laptop the moment
# its external display went idle.
write_monitors '[{"name":"eDP-1","disabled":true,"dpmsStatus":false},{"name":"USB-2","disabled":false,"dpmsStatus":false}]'
sync_inhibit
held || fail "a USB-C display blanked by DPMS keeps the inhibitor" "$(<"$call_log")"
pass "a USB-C display blanked by DPMS keeps the inhibitor"
# Hyprland misses queries while it reconfigures outputs, which is exactly what
# a lid close sets off. Releasing on a missed answer would suspend the machine.
touch "$hyprctl_fail"
sync_inhibit || fail "an unanswered compositor query is not an error"
held || fail "an unanswered compositor query keeps the inhibitor"
rm -f "$hyprctl_fail"
write_monitors ''
sync_inhibit || fail "an empty compositor answer is not an error"
held || fail "an empty compositor answer keeps the inhibitor"
write_monitors 'HYPRLAND_INSTANCE_SIGNATURE not set!'
sync_inhibit || fail "a compositor answer that is not JSON is not an error"
held || fail "a compositor answer that is not JSON keeps the inhibitor"
write_monitors "$clamshell"
pass "a compositor that does not answer changes nothing"
# logind can miss a query too, and the release would suspend the machine just the same.
touch "$busctl_fail"
sync_inhibit || fail "an unanswered logind query is not an error"
held || fail "an unanswered logind query keeps the inhibitor"
rm -f "$busctl_fail"
pass "logind not answering changes nothing"
# Unplugging is read from the connector, so it releases even when the compositor
# cannot be asked.
write_connectors card2-eDP-1 connected card2-USB-2 disconnected
touch "$hyprctl_fail"
sync_inhibit
! held || fail "unplugging the USB-C display releases the inhibitor"
(( $(count_calls stop) == 1 )) || fail "the inhibitor is released once" "$(<"$call_log")"
pass "unplugging the USB-C display releases the inhibitor"
sync_inhibit
(( $(count_calls stop) == 1 && $(count_calls start) == 1 )) ||
fail "a sync with nothing held changes nothing" "$(<"$call_log")"
pass "a sync with nothing held changes nothing"
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
touch "$hyprctl_fail"
sync_inhibit || fail "an unanswered compositor query is not an error"
touch "$busctl_fail"
rm -f "$hyprctl_fail"
sync_inhibit || fail "an unanswered logind query is not an error"
[[ ! -s $call_log ]] || fail "an unanswered query takes no inhibitor" "$(<"$call_log")"
pass "an unanswered query takes no inhibitor either"
# A display left plugged in but switched off in the monitor config is not in use,
# and a closed lid should suspend as it would with that display on HDMI.
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors '[{"name":"eDP-1","disabled":false},{"name":"USB-2","disabled":true}]'
sync_inhibit
[[ ! -s $call_log ]] || fail "a display switched off in the monitor config takes no inhibitor" "$(<"$call_log")"
pass "a USB-C display switched off in the monitor config takes no inhibitor"
# Machines logind already handles must behave exactly as before.
reset_unit
write_connectors card2-eDP-1 connected
write_monitors '[{"name":"eDP-1","disabled":false}]'
sync_inhibit
[[ ! -s $call_log ]] || fail "the built-in panel alone takes no inhibitor" "$(<"$call_log")"
pass "the built-in panel alone takes no inhibitor"
reset_unit
write_connectors card0-eDP-1 connected card0-HDMI-A-1 connected card0-DP-1 connected
write_monitors '[{"name":"eDP-1","disabled":true},{"name":"HDMI-A-1","disabled":false},{"name":"DP-1","disabled":false}]'
sync_inhibit
[[ ! -s $call_log ]] || fail "displays logind counts take no inhibitor" "$(<"$call_log")"
pass "HDMI and DisplayPort displays, which logind counts, take no inhibitor"
reset_unit
write_connectors card2-USB-2 connected
write_monitors "$clamshell"
OMARCHY_TEST_LAPTOP=0 sync_inhibit
[[ ! -s $call_log ]] || fail "a machine without a lid takes no inhibitor" "$(<"$call_log")"
pass "a machine without a lid takes no inhibitor"
# Only where logind would ignore a docked lid is there anything to stand in for.
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors "$clamshell"
OMARCHY_TEST_DOCKED_ACTION=suspend sync_inhibit
[[ ! -s $call_log ]] || fail "a docked lid set to suspend takes no inhibitor" "$(<"$call_log")"
sync_inhibit
held || fail "the inhibitor is taken under the default docked action"
OMARCHY_TEST_DOCKED_ACTION=suspend sync_inhibit
! held || fail "changing the docked lid action away from ignore releases the inhibitor"
pass "the inhibitor follows logind's docked lid action"
# logind lists DVI-A and Composite, but a missing comma fuses them into one
# entry, so they are uncounted too.
for connector in DVI-A-1 Composite-1; do
reset_unit
write_connectors card0-LVDS-1 connected "card0-$connector" connected
write_monitors "[{\"name\":\"LVDS-1\",\"disabled\":true},{\"name\":\"$connector\",\"disabled\":false}]"
sync_inhibit
held || fail "$connector takes the inhibitor"
done
pass "every external connector type logind misses takes the inhibitor"
# Held alongside a counted display too, or unplugging that one would let logind
# suspend before the watcher could take over.
reset_unit
write_connectors card2-eDP-1 connected card2-HDMI-A-1 connected card2-USB-2 connected
write_monitors '[{"name":"eDP-1","disabled":true},{"name":"HDMI-A-1","disabled":false},{"name":"USB-2","disabled":false}]'
sync_inhibit
held || fail "a USB-C display beside an HDMI one still holds the inhibitor"
pass "a USB-C display beside an HDMI one still holds the inhibitor"
# The monitor watcher is what keeps the inhibitor in step as displays come and go.
cat >"$fake_bin/socat" <<'SH'
#!/bin/bash
exec cat "$OMARCHY_TEST_EVENTS"
SH
for command in omarchy-hyprland-monitor-clamshell omarchy-hyprland-monitor-external-active; do
cat >"$fake_bin/$command" <<'SH'
#!/bin/bash
exit 0
SH
done
cat >"$fake_bin/omarchy-hyprland-monitor-modeless" <<'SH'
#!/bin/bash
exit 1
SH
chmod +x "$fake_bin"/*
ln -s "$lid_inhibit" "$fake_bin/omarchy-system-lid-inhibit"
start_watcher() {
rm -f "$events"
mkfifo "$events"
PATH="$fake_bin:$PATH" \
XDG_RUNTIME_DIR="$test_tmp" \
HYPRLAND_INSTANCE_SIGNATURE=test \
OMARCHY_TEST_EVENTS="$events" \
"$ROOT/bin/omarchy-hyprland-monitor-watch" &
watch_pid=$!
exec {events_fd}>"$events"
}
await() {
local waited
for (( waited = 0; waited < 100; waited++ )); do
"$@" && return 0
sleep 0.05
done
return 1
}
released() {
! held
}
reset_unit
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors "$clamshell"
start_watcher
await held || fail "the watcher takes the inhibitor at startup with a USB-C display attached"
pass "the watcher takes the inhibitor at startup with a USB-C display attached"
write_connectors card2-eDP-1 connected card2-USB-2 disconnected
write_monitors '[{"name":"eDP-1","disabled":false}]'
printf 'monitorremovedv2>>1,USB-2,Display\n' >&"$events_fd"
await released || fail "the watcher releases the inhibitor when the USB-C display is removed"
pass "the watcher releases the inhibitor when the USB-C display is removed"
write_connectors card2-eDP-1 connected card2-USB-2 connected
write_monitors "$clamshell"
printf 'monitoraddedv2>>2,USB-2,Display\n' >&"$events_fd"
await held || fail "the watcher takes the inhibitor again when the USB-C display returns"
# Across the delayed retries and the docked poll, one unit stays the only one.
sleep 3.5
(( $(count_calls start) == 2 && $(count_calls stop) == 1 && $(count_calls refused) == 0 )) ||
fail "the watcher never stacks inhibitors across its retries and poll" "$(<"$call_log")"
pass "the watcher keeps one inhibitor across hotplugs, retries and its poll"
stop_watcher
+346
View File
@@ -0,0 +1,346 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
dispatch="$ROOT/bin/omarchy-lifecycle-dispatch"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
operations=(provision-prepare provision-commit provision-verify reset-prepare reset-verify reset-commit reset-rollback update-verify update-takeover luks-slots setup-boot)
apple_optional=()
for platform in aarch64-apple aarch64 x86; do
fake_platform "$tmp/$platform" "$platform"
done
mkdir -p "$tmp/contradiction/proc/device-tree"
printf '%s\0' apple,j416c qcom,x1e80100 >"$tmp/contradiction/proc/device-tree/compatible"
cp -r "$tmp/aarch64-apple/bin" "$tmp/contradiction/bin"
# A root-owned directory of entrypoints stands in for the boot package; in a
# fixture root the caller's own files count as root's.
implementation=usr/lib/omarchy/mac-boot
install_implementation() {
local root=$1 operation
rm -rf "$root"
mkdir -p "$root/$implementation"
for operation in "${operations[@]}"; do
cat >"$root/$implementation/$operation" <<SH
#!/bin/bash
{ printf '%s' "$operation"; printf ' %q' "\$@"; echo; } >>"$tmp/ran"
env >"$tmp/env"
[[ ! -e $tmp/fail-with ]] || exit "\$(cat "$tmp/fail-with")"
SH
chmod 755 "$root/$implementation/$operation"
done
chmod -R go-w "$root"
}
# Runs the dispatcher on a platform fixture: $1 platform, $2 lifecycle root.
on() {
local platform=$1 root=$2
shift 2
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$root" PATH="$tmp/$platform/bin:$PATH" \
"$dispatch" "$@"
}
# -p in the shebang is what keeps a root caller's exported functions and
# BASH_ENV out, so an ordinary Bash launch with a decoy -p argument is refused
# before it resolves anything.
status=0
output=$(/usr/bin/bash "$dispatch" -p 2>&1) || status=$?
(( status == 126 )) && [[ $output == "Refusing an unsafe Bash startup." ]] ||
fail "an ordinary Bash launch with a decoy -p is refused" "status $status: $output"
pass "an ordinary Bash launch with a decoy -p is refused"
require_platform_fixtures "lifecycle dispatch on platform fixtures"
full=$tmp/full
install_implementation "$full"
empty=$tmp/empty
mkdir -p "$empty"
# x86 and plain aarch64 register no boot package: every operation
# is a no-op there, even with Mac entrypoints on disk that would fail.
echo 9 >"$tmp/fail-with"
for platform in x86 aarch64; do
for operation in "${operations[@]}"; do
rm -f "$tmp/ran"
output=$(on "$platform" "$full" "$operation" --flag 2>&1) || fail "$platform: $operation is a no-op" "$output"
[[ -z $output && ! -e $tmp/ran ]] || fail "$platform: $operation runs nothing and says nothing" "$output"
output=$(on "$platform" "$full" --resolve "$operation" 2>&1) || fail "$platform: $operation resolves" "$output"
[[ -z $output ]] || fail "$platform: $operation resolves to nothing" "$output"
done
pass "$platform: every operation is a no-op and no Mac entrypoint runs"
done
rm -f "$tmp/fail-with"
# Apple with omarchy-mac-boot: each operation runs its entrypoint with the
# caller's arguments, a cleared environment and a fixed PATH.
export CALLER_SECRET=leak
for operation in "${operations[@]}"; do
rm -f "$tmp/ran"
on aarch64-apple "$full" "$operation" first "second arg" || fail "apple: $operation runs its entrypoint"
[[ $(cat "$tmp/ran") == "$operation first second\\ arg" ]] || fail "apple: $operation passes its arguments" "$(cat "$tmp/ran")"
! grep -q CALLER_SECRET "$tmp/env" || fail "apple: $operation does not pass the caller's environment" "$(cat "$tmp/env")"
grep -qx 'PATH=/usr/local/sbin:/usr/local/bin:/usr/bin' "$tmp/env" || fail "apple: $operation runs with a fixed PATH" "$(cat "$tmp/env")"
resolved=$(on aarch64-apple "$full" --resolve "$operation") || fail "apple: $operation resolves"
[[ $resolved == "$full/$implementation/$operation" ]] || fail "apple: $operation resolves to its entrypoint" "$resolved"
done
unset CALLER_SECRET
echo 7 >"$tmp/fail-with"
status=0
on aarch64-apple "$full" provision-commit || status=$?
(( status == 7 )) || fail "apple: the entrypoint's exit status is the dispatcher's" "status: $status"
rm -f "$tmp/fail-with"
pass "apple: each operation runs the boot package's entrypoint with its arguments and status"
# Apple without omarchy-mac-boot: required operations fail naming the package
# and entrypoint; optional ones are no-ops.
for operation in "${operations[@]}"; do
status=0
output=$(on aarch64-apple "$empty" "$operation" 2>&1) || status=$?
if [[ " ${apple_optional[*]} " == *" $operation "* ]]; then
(( status == 0 )) && [[ -z $output ]] || fail "apple: optional $operation is a no-op without the boot package" "$output"
output=$(on aarch64-apple "$empty" --resolve "$operation" 2>&1) && [[ -z $output ]] ||
fail "apple: optional $operation resolves to nothing without the boot package" "$output"
else
(( status == 3 )) || fail "apple: required $operation fails with status 3 without the boot package" "status: $status"
[[ $output == "Error: $operation on aarch64-apple needs omarchy-mac-boot, which provides /usr/lib/omarchy/mac-boot/$operation; it is not installed" ]] ||
fail "apple: required $operation names the missing package and entrypoint" "$output"
status=0
on aarch64-apple "$empty" --resolve "$operation" >/dev/null 2>&1 || status=$?
(( status == 3 )) || fail "apple: required $operation does not resolve without the boot package" "status: $status"
fi
done
pass "apple: without omarchy-mac-boot required operations fail with a clear message and optional ones are no-ops"
# An installed omarchy-mac-boot that predates an operation is named with its
# version, as an update away rather than missing.
older=$tmp/older
mkdir -p "$older/usr/lib/omarchy/mac-boot" "$older/var/lib/pacman/local/omarchy-mac-boot-20260921-10"
for operation in "${operations[@]}"; do
[[ " ${apple_optional[*]} " == *" $operation "* ]] && continue
status=0
output=$(on aarch64-apple "$older" "$operation" 2>&1) || status=$?
(( status == 1 )) && [[ $output == "Error: $operation on aarch64-apple needs /usr/lib/omarchy/mac-boot/$operation, which omarchy-mac-boot 20260921-10 does not provide; update omarchy-mac-boot" ]] ||
fail "apple: an omarchy-mac-boot without $operation is named with its version" "status $status: $output"
done
pass "apple: an installed omarchy-mac-boot that lacks a required operation fails asking for its update"
# An entrypoint anyone but root could have changed never runs, optional or not.
untrusted() {
local description=$1 operation=$2
rm -f "$tmp/ran"
if output=$(on aarch64-apple "$full" "$operation" 2>&1); then
fail "apple: $description is refused"
fi
[[ ! -e $tmp/ran ]] || fail "apple: $description never runs"
[[ $output == *"refusing /usr/lib/omarchy/mac-boot/$operation"* ]] || fail "apple: $description is named" "$output"
if on aarch64-apple "$full" --resolve "$operation" >/dev/null 2>&1; then
fail "apple: $description does not resolve"
fi
}
install_implementation "$full"
chmod g+w "$full/$implementation/reset-commit"
untrusted "a group-writable entrypoint" reset-commit
install_implementation "$full"
chmod o+w "$full/$implementation/provision-commit"
untrusted "a world-writable entrypoint" provision-commit
install_implementation "$full"
chmod o+w "$full/usr/lib/omarchy"
untrusted "an entrypoint in a world-writable directory" provision-verify
install_implementation "$full"
mv "$full/$implementation/provision-prepare" "$tmp/elsewhere"
ln -s "$tmp/elsewhere" "$full/$implementation/provision-prepare"
untrusted "a symlinked entrypoint" provision-prepare
install_implementation "$full"
chmod 644 "$full/$implementation/update-verify"
untrusted "a non-executable entrypoint" update-verify
install_implementation "$full"
rm "$full/$implementation/reset-verify"
mkdir "$full/$implementation/reset-verify"
chmod 755 "$full/$implementation/reset-verify"
untrusted "a directory in place of an entrypoint" reset-verify
pass "apple: an entrypoint that is not a root-owned file in root-owned directories never runs"
install_implementation "$full"
for arguments in "" "unknown-operation" "--resolve" "--resolve unknown-operation"; do
rm -f "$tmp/ran"
status=0
output=$(on aarch64-apple "$full" $arguments 2>&1) || status=$?
(( status == 2 )) && [[ $output == Usage:* ]] || fail "'$arguments' is a usage error" "status $status: $output"
[[ ! -e $tmp/ran ]] || fail "'$arguments' runs nothing"
done
status=0
output=$(on aarch64-apple "$full" "provision-prepare provision-commit" 2>&1) || status=$?
(( status == 2 )) && [[ ! -e $tmp/ran ]] || fail "two operation names in one argument are a usage error" "status $status: $output"
pass "an operation outside the fixed set is a usage error"
rm -f "$tmp/ran"
if output=$(on contradiction "$full" provision-commit 2>&1); then
fail "a platform the detector cannot settle fails the operation"
fi
[[ ! -e $tmp/ran && $output == *"cannot determine the hardware platform for provision-commit"* ]] ||
fail "an undetermined platform runs nothing and says why" "$output"
pass "an undetermined platform fails closed"
rm -f "$tmp/ran"
if output=$(cd "$tmp" && on aarch64-apple full provision-commit 2>&1); then
fail "a relative fixture root is refused"
fi
[[ ! -e $tmp/ran && $output == "Error: OMARCHY_LIFECYCLE_ROOT must be an absolute path" ]] ||
fail "a relative fixture root runs nothing and says why" "$output"
pass "a relative fixture root is refused"
# Root resolves only the fixed /usr/lib path, whatever fixture root its
# environment names. A copy beside a detector that always answers Apple keeps
# the live platform out of the way.
if unshare --user --map-root-user true 2>/dev/null; then
mkdir -p "$tmp/rootbin"
cp "$dispatch" "$tmp/rootbin/"
printf '#!/bin/bash\necho aarch64-apple\n' >"$tmp/rootbin/omarchy-hw-platform"
chmod +x "$tmp/rootbin/omarchy-hw-platform"
rm -f "$tmp/ran"
status=0
output=$(OMARCHY_LIFECYCLE_ROOT="$full" unshare --user --map-root-user "$tmp/rootbin/omarchy-lifecycle-dispatch" reset-prepare 2>&1) ||
status=$?
(( status != 0 )) && [[ ! -e $tmp/ran && $output != *"$tmp"* && $output == *" /usr/lib/omarchy/mac-boot/reset-prepare"* ]] ||
fail "root ignores a fixture root in its environment" "status $status: $output"
pass "root ignores fixture roots when resolving an operation"
printf 'touch %q\n' "$tmp/bash-env-ran" >"$tmp/bash-env"
dirname() { touch "$tmp/function-ran"; echo /nonexistent; }
export -f dirname
BASH_ENV="$tmp/bash-env" unshare --user --map-root-user "$dispatch" --resolve provision-commit >/dev/null 2>&1 || true
unset -f dirname
[[ ! -e $tmp/bash-env-ran && ! -e $tmp/function-ran ]] || fail "root runs no code from BASH_ENV or exported functions"
pass "root runs no code from BASH_ENV or exported functions"
else
skip "no unprivileged user namespace; skipping the root override probe"
fi
# ── setup and app-install operations ─────────────────────────────────────────
# System and user setup and the app-install hooks resolve in omarchy-mac's
# directory on a Mac, never in omarchy-mac-boot's, and are optional everywhere.
setup_operations=(setup-system setup-user post-install pre-remove)
user_operations=(setup-user post-install pre-remove)
setup_implementation=usr/lib/omarchy/mac
install_setup() {
local root=$1 dir=$2 operation
mkdir -p "$root/$dir"
for operation in "${setup_operations[@]}"; do
cat >"$root/$dir/$operation" <<SH
#!/bin/bash
{ printf '%s' "$operation"; (( \$# == 0 )) || printf ' %q' "\$@"; echo; } >>"$tmp/ran"
env >"$tmp/env"
[[ ! -e $tmp/fail-with ]] || exit "\$(cat "$tmp/fail-with")"
SH
chmod 755 "$root/$dir/$operation"
done
chmod -R go-w "$root"
}
with_mac=$tmp/with-mac
rm -rf "$with_mac"
install_setup "$with_mac" "$setup_implementation"
boot_only=$tmp/boot-only
rm -rf "$boot_only"
install_setup "$boot_only" "$implementation"
echo 9 >"$tmp/fail-with"
for platform in x86 aarch64; do
for operation in "${setup_operations[@]}"; do
rm -f "$tmp/ran"
output=$(on "$platform" "$with_mac" "$operation" 2>&1) && [[ -z $output && ! -e $tmp/ran ]] ||
fail "$platform: $operation is a no-op" "$output"
output=$(on "$platform" "$with_mac" --resolve "$operation" 2>&1) && [[ -z $output ]] ||
fail "$platform: $operation resolves to nothing" "$output"
done
done
rm -f "$tmp/fail-with"
pass "x86 and plain aarch64: setup and app-install operations are no-ops, even with Mac entrypoints on disk"
for operation in "${setup_operations[@]}"; do
rm -f "$tmp/ran"
resolved=$(on aarch64-apple "$with_mac" --resolve "$operation") && [[ $resolved == "$with_mac/$setup_implementation/$operation" ]] ||
fail "apple: $operation resolves to omarchy-mac's entrypoint" "$resolved"
output=$(on aarch64-apple "$empty" "$operation" 2>&1) && [[ -z $output ]] ||
fail "apple: $operation is a no-op without omarchy-mac" "$output"
output=$(on aarch64-apple "$boot_only" "$operation" 2>&1) && [[ -z $output && ! -e $tmp/ran ]] ||
fail "apple: $operation never runs from omarchy-mac-boot's directory" "$output"
done
pass "apple: setup and the app-install hooks resolve in omarchy-mac's directory, and are no-ops without omarchy-mac"
session=(CALLER_SECRET=leak HOME=/home/owner USER=owner XDG_RUNTIME_DIR=/run/user/1000 XDG_CONFIG_HOME=/home/owner/.cfg
XDG_STATE_HOME=/home/owner/.st XDG_DATA_HOME=/home/owner/.data DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus OMARCHY_PATH=/usr/share/omarchy)
rm -f "$tmp/ran"
env -u WAYLAND_DISPLAY "${session[@]}" OMARCHY_PROC_ROOT="$tmp/aarch64-apple/proc" OMARCHY_LIFECYCLE_ROOT="$with_mac" \
PATH="$tmp/aarch64-apple/bin:$PATH" "$dispatch" setup-system image-first-boot || fail "apple: setup-system runs"
[[ $(cat "$tmp/ran") == "setup-system image-first-boot" ]] || fail "apple: setup-system gets its argument" "$(cat "$tmp/ran")"
[[ $(grep -Ev '^(_|PWD|OLDPWD|SHLVL)=' "$tmp/env" | sort) == "PATH=/usr/local/sbin:/usr/local/bin:/usr/bin" ]] ||
fail "apple: setup-system gets PATH alone" "$(cat "$tmp/env")"
expected=$(printf '%s\n' DBUS_SESSION_BUS_ADDRESS=unix:path=/run/user/1000/bus HOME=/home/owner OMARCHY_PATH=/usr/share/omarchy \
PATH=/usr/local/sbin:/usr/local/bin:/usr/bin USER=owner XDG_CONFIG_HOME=/home/owner/.cfg XDG_RUNTIME_DIR=/run/user/1000 XDG_STATE_HOME=/home/owner/.st)
for invocation in "setup-user" "post-install steam" "pre-remove steam"; do
rm -f "$tmp/ran"
env -u WAYLAND_DISPLAY "${session[@]}" OMARCHY_PROC_ROOT="$tmp/aarch64-apple/proc" OMARCHY_LIFECYCLE_ROOT="$with_mac" \
PATH="$tmp/aarch64-apple/bin:$PATH" "$dispatch" $invocation || fail "apple: $invocation runs"
[[ $(cat "$tmp/ran") == "$invocation" ]] || fail "apple: $invocation runs its entrypoint with its argument" "$(cat "$tmp/ran")"
[[ $(grep -Ev '^(_|PWD|OLDPWD|SHLVL)=' "$tmp/env" | sort) == "$expected" ]] ||
fail "apple: $invocation gets the user's home and session, and nothing else" "$(cat "$tmp/env")"
done
echo 3 >"$tmp/fail-with"
status=0
on aarch64-apple "$with_mac" setup-user || status=$?
(( status == 3 )) || fail "apple: a setup entrypoint's own status 3 passes through" "status $status"
rm -f "$tmp/fail-with"
pass "apple: setup-system gets PATH alone and the user operations the user's home and session, with the entrypoint's status"
chmod o+w "$with_mac/$setup_implementation/setup-user"
rm -f "$tmp/ran"
if output=$(on aarch64-apple "$with_mac" setup-user 2>&1); then
fail "apple: a world-writable setup entrypoint is refused"
fi
[[ ! -e $tmp/ran && $output == *"refusing /usr/lib/omarchy/mac/setup-user"* ]] ||
fail "apple: a world-writable setup entrypoint never runs" "$output"
chmod o-w "$with_mac/$setup_implementation/setup-user"
pass "apple: a setup entrypoint that fails the trust rules never runs"
# Root is refused a user operation only where the platform registers it, so an
# installer run with sudo still finishes elsewhere. As root the dispatcher runs
# the detector beside it, so each platform gets a copy beside a stub.
if unshare --user --map-root-user true 2>/dev/null; then
for platform in aarch64-apple x86 aarch64; do
mkdir -p "$tmp/root-$platform"
cp "$dispatch" "$tmp/root-$platform/"
printf '#!/bin/bash\necho %s\n' "$platform" >"$tmp/root-$platform/omarchy-hw-platform"
chmod +x "$tmp/root-$platform/omarchy-hw-platform"
for operation in "${user_operations[@]}"; do
for arguments in "$operation" "--resolve $operation" "$operation steam"; do
rm -f "$tmp/ran"
status=0
output=$(OMARCHY_LIFECYCLE_ROOT="$with_mac" unshare --user --map-root-user \
"$tmp/root-$platform/omarchy-lifecycle-dispatch" $arguments 2>&1) || status=$?
[[ ! -e $tmp/ran ]] || fail "$platform: root never runs '$arguments'"
if [[ $platform == "aarch64-apple" ]]; then
(( status == 1 )) && [[ $output == "Error: $operation runs as the user, never as root" ]] ||
fail "apple: root is refused '$arguments'" "status $status: $output"
else
(( status == 0 )) && [[ -z $output ]] || fail "$platform: '$arguments' is a no-op for root" "status $status: $output"
fi
done
done
done
pass "setup-user, post-install and pre-remove refuse root on Apple Silicon, and are no-ops for root elsewhere"
else
skip "no unprivileged user namespace; skipping the root refusal of the user operations"
fi
+4
View File
@@ -10,6 +10,10 @@ grep -Fq 'KERNEL_CMDLINE[default]+=" initramfs_async=0"' "$packaged_defaults" ||
fail "the packaged Limine defaults still unpack the initramfs synchronously"
pass "packaged Limine defaults keep Plymouth alive at the LUKS prompt"
grep -Fxq 'KERNEL_CMDLINE[default]+=" systemd.tty.term.console=dumb"' "$packaged_defaults" ||
fail "the packaged Limine defaults let PID 1 wait for the console to answer a size query"
pass "packaged Limine defaults keep PID 1 from waiting on the console"
grep -Fxq 'BOOT_ORDER="linux-t2, linux-omarchy, linux-omarchy-*, *, *fallback, Snapshots"' "$packaged_defaults" ||
fail "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere"
pass "packaged Limine defaults protect T2 Macs and prefer the exact Omarchy kernel elsewhere"
+121
View File
@@ -0,0 +1,121 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
leaf="$ROOT/install/config/locale.sh"
[[ -f $leaf ]] || fail "the locale step ships"
grep -Fq 'config/locale.sh' "$ROOT/install/config/all.sh" ||
fail "config phase sets a UTF-8 locale"
pass "config phase sets a UTF-8 locale"
# Root always writes the real locale files: the overrides below are for
# unprivileged runs, so as root the step would change this machine's locale.
if (( EUID == 0 )); then
skip "running as root, where the locale step ignores its fixture; skipping"
exit 0
fi
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
calls="$test_tmp/calls.log"
locale_conf="$test_tmp/etc/locale.conf"
locale_gen="$test_tmp/etc/locale.gen"
mkdir -p "$stub_bin" "$test_tmp/etc"
# GENERATED_LOCALES stands in for what the machine already has built.
cat >"$stub_bin/locale" <<'SH'
#!/bin/bash
[[ ${1:-} == "-a" ]] || exit 0
printf '%s\n' ${GENERATED_LOCALES:-C}
SH
cat >"$stub_bin/locale-gen" <<'SH'
#!/bin/bash
printf 'locale-gen\n' >>"$TEST_LOG"
SH
chmod +x "$stub_bin"/*
run_leaf() {
: >"$calls"
GENERATED_LOCALES="${1:-C}" TEST_LOG="$calls" PATH="$stub_bin:$PATH" \
OMARCHY_LOCALE_CONF="$locale_conf" OMARCHY_LOCALE_GEN="$locale_gen" \
bash -euo pipefail -c 'source "$1"' bash "$leaf" >/dev/null
}
# A root from a distribution tarball: LANG=C, nothing enabled in locale.gen.
printf 'LANG=C\n' >"$locale_conf"
printf '#en_US.UTF-8 UTF-8\n#de_DE.UTF-8 UTF-8\n' >"$locale_gen"
run_leaf C
/usr/bin/grep -qx 'LANG=en_US.UTF-8' "$locale_conf" || fail "a C locale is replaced with UTF-8" "$(cat "$locale_conf")"
/usr/bin/grep -qx 'en_US.UTF-8 UTF-8' "$locale_gen" || fail "en_US.UTF-8 is enabled in locale.gen" "$(cat "$locale_gen")"
/usr/bin/grep -qx '#de_DE.UTF-8 UTF-8' "$locale_gen" || fail "other locales are left commented" "$(cat "$locale_gen")"
/usr/bin/grep -qx 'locale-gen' "$calls" || fail "the locale is generated" "$(cat "$calls")"
pass "a stock LANG=C machine gets en_US.UTF-8"
# Only the stock state is repaired. Every named locale is somebody's choice --
# including C.UTF-8, and including one that is not UTF-8 at all.
for chosen in en_DK.UTF-8 C.UTF-8 en_US.ISO-8859-1; do
printf 'LANG=%s\n' "$chosen" >"$locale_conf"
run_leaf "C en_DK.utf8 C.utf8"
/usr/bin/grep -qx "LANG=$chosen" "$locale_conf" || fail "a chosen locale is left alone" "$chosen -> $(cat "$locale_conf")"
[[ ! -s $calls ]] || fail "a chosen locale is not regenerated" "$chosen: $(cat "$calls")"
done
pass "a machine with a chosen locale is left alone"
# POSIX is the same stock state under another name, and a missing file means
# the system never had one set.
printf 'LANG=POSIX\n' >"$locale_conf"
run_leaf C
/usr/bin/grep -qx 'LANG=en_US.UTF-8' "$locale_conf" || fail "a POSIX locale is repaired" "$(cat "$locale_conf")"
rm -f "$locale_conf"
run_leaf C
/usr/bin/grep -qx 'LANG=en_US.UTF-8' "$locale_conf" || fail "a machine with no locale.conf is repaired" "$(cat "$locale_conf" 2>&1)"
pass "POSIX and an absent locale.conf count as stock"
# Already generated, just not selected: set it without a rebuild.
printf 'LANG=C\n' >"$locale_conf"
run_leaf "C en_US.utf8"
/usr/bin/grep -qx 'LANG=en_US.UTF-8' "$locale_conf" || fail "an available locale is selected" "$(cat "$locale_conf")"
[[ ! -s $calls ]] || fail "a generated locale is not regenerated" "$(cat "$calls")"
pass "a generated locale is selected without rebuilding"
# Other locale variables somebody set stay as they are.
printf 'LANG=C\nLC_TIME=en_DK.UTF-8\nLC_PAPER=de_DE.UTF-8\n' >"$locale_conf"
run_leaf "C en_US.utf8"
[[ $(cat "$locale_conf") == $'LANG=en_US.UTF-8\nLC_TIME=en_DK.UTF-8\nLC_PAPER=de_DE.UTF-8' ]] ||
fail "only LANG is repaired; LC_* settings stay" "$(cat "$locale_conf")"
printf 'LC_TIME=en_DK.UTF-8\n' >"$locale_conf"
run_leaf "C en_US.utf8"
[[ $(cat "$locale_conf") == $'LC_TIME=en_DK.UTF-8\nLANG=en_US.UTF-8' ]] ||
fail "an unset LANG is added beside the LC_* settings" "$(cat "$locale_conf")"
pass "only LANG is repaired; LC_* settings stay"
printf 'LANG=C\n' >"$locale_conf"
run_leaf "C en_US.utf8"
# Second pass over a repaired machine changes nothing.
run_leaf "C en_US.utf8"
(( $(/usr/bin/grep -c . "$locale_conf") == 1 )) || fail "the locale step is idempotent" "$(cat "$locale_conf")"
pass "the locale step is idempotent"
# The x86 ISO writes the locale the owner picked before this runs, so there the
# step changes nothing.
printf 'LANG=en_US.UTF-8\n' >"$locale_conf"
printf '#en_US.UTF-8 UTF-8\n' >"$locale_gen"
: >"$calls"
output=$(GENERATED_LOCALES="C en_US.utf8" TEST_LOG="$calls" PATH="$stub_bin:$PATH" \
OMARCHY_LOCALE_CONF="$locale_conf" OMARCHY_LOCALE_GEN="$locale_gen" \
bash -euo pipefail -c 'source "$1"' bash "$leaf")
[[ $output == "Leaving the locale as en_US.UTF-8" && $(cat "$locale_conf") == "LANG=en_US.UTF-8" ]] ||
fail "an ISO install's locale is left as the ISO wrote it" "$output"
[[ $(cat "$locale_gen") == "#en_US.UTF-8 UTF-8" && ! -s $calls ]] || fail "an ISO install's locale.gen is not touched"
pass "an ISO install's locale is left as the ISO wrote it"
@@ -0,0 +1,71 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
findmnt() {
[[ $* == "-no SOURCE /" ]] || return 99
printf '%s\n' "$root_source"
}
lsblk() {
[[ ${*: -1} == "/dev/mapper/root" ]] || return 98
printf '%s\n' "$*" >>"$test_tmp/lsblk-calls"
[[ $disk_state != "failure" ]] || return 1
if [[ $disk_state == "lvm" ]]; then
printf '/dev/mapper/root btrfs\n/dev/mapper/cryptlvm LVM2_member\n/dev/nvme0n1p2 crypto_LUKS\n/dev/nvme0n1 \n'
elif [[ $disk_state == "encrypted" ]]; then
if [[ $1 == -*r* ]]; then
printf '/dev/mapper/root btrfs\n/dev/nvme0n1p6 crypto_LUKS\n/dev/nvme0n1 \n'
else
printf '/dev/mapper/root btrfs\n└─/dev/nvme0n1p6 crypto_LUKS\n └─/dev/nvme0n1 \n'
fi
else
printf '/dev/mapper/root btrfs\n/dev/nvme0n1p6 ext4\n'
fi
}
for script in omarchy-provision-owner omarchy-system-factory-reset; do
# Load only this read-only helper, never the provisioning/reset entrypoint.
sed -n '/^luks_device() {/,/^}/p' "$ROOT/bin/$script" |
sed "s|/proc/cmdline|$test_tmp/cmdline|g" >"$test_tmp/helper"
[[ -s $test_tmp/helper ]] || fail "$script exposes its LUKS lookup"
source "$test_tmp/helper"
printf 'root=/dev/mapper/root rd.luks.name=fixture=root\n' >"$test_tmp/cmdline"
root_source='/dev/mapper/root[/@]'
disk_state=encrypted
actual=$(luks_device) || fail "$script resolves a crypttab root"
[[ $actual == "/dev/nvme0n1p6" ]] || fail "$script returns the raw partition path" "$actual"
pass "$script strips the btrfs subvolume and returns the LUKS parent without tree glyphs"
disk_state=lvm
actual=$(luks_device) || fail "$script resolves a root on LVM on LUKS"
[[ $actual == "/dev/nvme0n1p2" ]] || fail "$script walks past the LVM layer to the LUKS partition" "$actual"
pass "$script finds the LUKS partition beneath an LVM volume group"
for disk_state in plain failure; do
if actual=$(luks_device); then
fail "$script rejects $disk_state ancestry" "$actual"
fi
[[ -z $actual ]] || fail "$script emits no device on $disk_state ancestry"
done
root_source=""
if luks_device; then fail "$script rejects a missing root source"; fi
for spec in UUID=fixture PARTUUID=fixture LABEL=fixture PARTLABEL=fixture /dev/nvme0n1p6; do
case $spec in
UUID=*) expected=/dev/disk/by-uuid/fixture ;;
PARTUUID=*) expected=/dev/disk/by-partuuid/fixture ;;
LABEL=*) expected=/dev/disk/by-label/fixture ;;
PARTLABEL=*) expected=/dev/disk/by-partlabel/fixture ;;
*) expected=$spec ;;
esac
: >"$test_tmp/lsblk-calls"
printf 'cryptdevice=%s:root root=/dev/mapper/root\n' "$spec" >"$test_tmp/cmdline"
actual=$(luks_device) || fail "$script preserves $spec lookup"
[[ $actual == "$expected" && ! -s $test_tmp/lsblk-calls ]] || fail "$script prefers the explicit cryptdevice"
done
pass "$script preserves explicit cryptdevice lookup and rejects unencrypted or missing roots"
done
+904
View File
@@ -0,0 +1,904 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Owner provisioning's LUKS re-key, killed after every durable step and rerun.
# Each attempt is its own process, like a reboot: omarchy-provision-owner's
# setup attempt from the accepted owner form on, the shared re-key, and a
# cryptsetup that is either a slot-table fake or the real binary on a
# file-backed LUKS2 or LUKS1 volume. The callbacks reach the platform through
# the real omarchy-lifecycle-dispatch: on an x86 fixture it is a no-op and the
# Limine UKI path runs; on an Apple fixture a fake boot package with
# provisioning and luks-slots entrypoints owns an unlock kept on a boot
# partition and the kernel command line and records the kept slot.
tmp=$(mktemp -d)
token_key=""
trap 'rm -rf "$tmp"; [[ -z $token_key ]] || keyctl unlink "$token_key" @s >/dev/null 2>&1 || true' EXIT
fake_platform "$tmp/x86" x86
fake_platform "$tmp/apple" aarch64-apple
platform=x86
# Root's dispatcher ignores the fixtures and sees this machine, which stands in
# for x86 only where no boot package is registered.
platforms=(x86 apple)
if (( EUID == 0 )); then
if [[ $("$ROOT/bin/omarchy-hw-platform") == "aarch64-apple" ]]; then
skip "running as root on Apple Silicon, where dispatch ignores fixtures; skipping"
exit 0
fi
platforms=(x86)
skip "running as root, where dispatch ignores fixtures; skipping the Apple runs"
fi
runtime=$ROOT
mac_boot=$tmp/lifecycle/usr/lib/omarchy/mac-boot
mkdir -p "$mac_boot"
cat >"$tmp/mac-boot-lib.sh" <<'SH'
ran() { echo "$1" >>"$TMP/mac-boot-ran"; }
crash() {
local count
count=$(( $(cat "$TMP/steps") + 1 ))
echo "$count" >"$TMP/steps"
printf '%s %s\n' "$count" "$1" >>"$TMP/trace"
if (( count == $(cat "$TMP/crash-at") )); then
kill -9 "$PPID"
kill -9 $$
fi
}
SH
mac_boot_entrypoint() {
{ printf '#!/bin/bash\nset -euo pipefail\nTMP=%q\nsource "$TMP/mac-boot-lib.sh"\nran %s\n' "$tmp" "$1"; cat; } >"$mac_boot/$1"
}
mac_boot_entrypoint provision-prepare <<'SH'
if [[ -e $TMP/prepare-fail ]]; then
echo "The boot partition is not mounted." >&2
exit 1
fi
SH
mac_boot_entrypoint provision-commit <<'SH'
[[ ! -e $TMP/rebuild-fail ]] || exit 1
rm -f "$TMP/boot/omarchy/luks-key"
crash "boot-partition key removed"
sed -i 's/ rd\.luks\.key=[^" ]*//' "$TMP/etc/default/grub"
echo rebuild >>"$TMP/rebuilds"
crash "boot rebuilt"
SH
mac_boot_entrypoint provision-verify <<'SH'
[[ ! -e $TMP/boot/omarchy/luks-key ]] && ! grep -q 'rd\.luks\.key=' "$TMP/etc/default/grub"
SH
mac_boot_entrypoint luks-slots <<'SH'
[[ ! -e $TMP/record-fail ]] || exit 1
printf '%s\n' "$@" >"$TMP/slot-record"
crash "slots recorded"
SH
chmod 755 "$mac_boot"/*
chmod -R go-w "$tmp/lifecycle"
staged_key=staged-install-key
seller_key=previous-owner-key
owner_password=owner-password
sed -n '/^PROVISIONING_UNLOCK_FILES=(/,/^)/p; /^UNLOCK_OWNER=/p; /^limine_auto_unlock_present() {/,/^}/p; /^limine_auto_unlock_drop() {/,/^}/p
/^unlock_owner() {/,/^}/p; /^luks_auto_unlock_present() {/,/^}/p; /^luks_auto_unlock_drop() {/,/^}/p
/^luks_record_slots() {/,/^}/p' \
"$ROOT/bin/omarchy-provision-owner" | sed "s|/etc/|$tmp/etc/|g" >"$tmp/unlock.sh"
grep -q '^luks_auto_unlock_drop() {' "$tmp/unlock.sh" && grep -q '^limine_auto_unlock_drop() {' "$tmp/unlock.sh" &&
grep -q '^luks_record_slots() {' "$tmp/unlock.sh" ||
fail "omarchy-provision-owner defines the dispatched and Limine auto-unlock callbacks and the slot record"
sed -n '/^rekey_luks() {/,/^}/p; /^run_provisioning() {/,/^}/p; /^cleanup_oem_state() {/,/^}/p
/^platform_ready() {/,/^}/p; /^run_setup() {/,/^}/p
/^rekey_accepts_password() {/,/^}/p' \
"$ROOT/bin/omarchy-provision-owner" |
sed -e "s|/etc/|$tmp/etc/|g" -e "s|/usr/bin/omarchy-thunderbolt-authorization-admin|omarchy-thunderbolt-authorization-admin|" >"$tmp/provision.sh"
grep -q '^run_provisioning() {' "$tmp/provision.sh" && grep -q '^run_setup() {' "$tmp/provision.sh" ||
fail "omarchy-provision-owner defines its setup and provisioning worker"
cat >"$tmp/attempt.sh" <<'SH'
set -euo pipefail
source "$ROOT/install/provisioning/luks-rekey.sh"
source "$TMP/unlock.sh"
source <(sed -n '/^luks_boot_layout() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner" | sed "s|/etc/|$TMP/etc/|g")
declare -F luks_boot_layout >/dev/null
PROVISIONING_DIR=$TMP/provisioning
REKEY_STATE=$PROVISIONING_DIR/luks-rekey.state
LOG_FILE=$TMP/log
password=$PASSWORD
log_step() { printf '%s\n' "$*" >>"$LOG_FILE"; }
say() { :; }
crash_point() {
local count
count=$(( $(cat "$TMP/steps") + 1 ))
echo "$count" >"$TMP/steps"
printf '%s %s\n' "$count" "$1" >>"$TMP/trace"
if (( count == ${CRASH_AT:-0} )); then
kill -9 $$
kill -9 "$BASHPID"
fi
}
eval "$(declare -f rekey_state_put | sed '1s/^rekey_state_put /journal_write /')"
rekey_state_put() {
journal_write "$@" || return 1
crash_point "journal $*"
}
reset_limine_config() {
echo reset >>"$TMP/limine-ran"
crash_point "auto-unlock files removed"
}
limine-update() {
echo update >>"$TMP/limine-ran"
echo rebuild >>"$TMP/rebuilds"
[[ ! -e $TMP/rebuild-fail ]] || return 1
crash_point "boot rebuilt"
}
shred() {
command rm -f -- "${@: -1}"
crash_point "staged key destroyed"
}
fake_cryptsetup() {
local op=$1 key_file="" token_type="" target="" extra="" requested="" material slot next
shift
while (( $# )); do
case $1 in
--key-file) key_file=$2; shift 2 ;;
--key-slot) requested=$2; shift 2 ;;
--token-type) token_type=$2; shift 2 ;;
-*) shift ;;
*)
if [[ -z $target ]]; then target=$1; else extra=$1; fi
shift
;;
esac
done
[[ $target == "$DEVICE" ]] || return 4
if [[ $op == "luksDump" ]]; then
echo "Keyslots:"
awk '{ printf " %s: luks2\n", $1 }' "$TMP/slots"
if [[ -s $TMP/token-slot ]]; then
printf 'Tokens:\n 0: luks2-keyring\n\tKeyslot: %s\n' "$(cat "$TMP/token-slot")"
fi
printf 'Digests:\n 0: pbkdf2\n'
return 0
fi
if [[ $op == "luksKillSlot" ]]; then
[[ ! -e $TMP/kill-noop ]] || return 0
awk -v s="$extra" '$1 != s' "$TMP/slots" >"$TMP/slots.next"
command mv -f "$TMP/slots.next" "$TMP/slots"
return 0
fi
# An enrolled token unlocks its slot whatever key is given, unless the
# allowed token types exclude it, as with cryptsetup.
if [[ $op == "open" && -z $token_type && -s $TMP/token-slot ]]; then
echo "Key slot $(cat "$TMP/token-slot") unlocked."
return 0
fi
material=$(cat "$key_file")
slot=$(awk -v m="$material" '$2 == m { print $1; exit }' "$TMP/slots")
if [[ -z $slot ]]; then
echo "No key available with this passphrase."
return 2
fi
case $op in
open) echo "Key slot $slot unlocked." ;;
luksAddKey)
next=$requested
if [[ -z $next ]]; then
for (( next = 0; next < 32; next++ )); do
awk '{ print $1 }' "$TMP/slots" | grep -qx "$next" || break
done
fi
! awk '{ print $1 }' "$TMP/slots" | grep -qx "$next" || return 1
printf '%s %s\n' "$next" "$(cat "$extra")" >>"$TMP/slots"
;;
*) return 1 ;;
esac
}
cryptsetup() {
local status=0
if [[ $BACKEND == "fake" ]]; then
fake_cryptsetup "$@" || status=$?
elif [[ $1 == "luksAddKey" ]]; then
command cryptsetup luksAddKey --pbkdf pbkdf2 --pbkdf-force-iterations 1000 "${@:2}" || status=$?
else
command cryptsetup "$@" || status=$?
fi
(( status == 0 )) || return "$status"
case $1 in
luksAddKey)
echo owner >>"$TMP/adds"
crash_point "owner key added"
;;
luksKillSlot) crash_point "slot ${*: -1} killed" ;;
esac
}
# omarchy-provision-owner's setup functions, with account and boot setup and
# the screen stubbed.
setup_functions() {
source "$TMP/provision.sh"
STATE_FILE=$TMP/state
FINALIZE_WARNING_FLAG=$TMP/finalize-warning
username=owner hostname="" timezone=""
create_user() { :; }
install_authorized_keys() { :; }
configure_login() { :; }
configure_hostname() { :; }
configure_timezone() { :; }
finalize_user() { :; }
# Accessory enrollment, recorded with whether the staged unlock was gone.
omarchy-pkg-present() { return 1; }
omarchy-thunderbolt-authorization-admin() {
if luks_staged_unlock_remains; then echo "$* early" >>"$TMP/accessories"; else echo "$*" >>"$TMP/accessories"; fi
}
limine_entries_stale() {
crash_point "re-key returned"
[[ -e $TMP/stale ]]
}
luks_device() { echo "$DEVICE"; }
systemctl() { :; }
clear_logo() { :; }
sleep() { :; }
say() { printf '%s\n' "$*" >>"$TMP/screen"; }
}
# The first-boot worker around the re-key.
provision() {
setup_functions
run_provisioning
}
# A whole setup attempt once the owner form is filled in: the password check,
# then the worker.
attempt() {
setup_functions
keyboard_form() { :; }
user_form() { :; }
confirm_form() { :; }
notice() {
printf 'refused: %s\n' "$1" >>"$TMP/screen"
exit 3
}
set_now() { NOW=0; }
render_setup_static() { :; }
render_setup_dynamic() { :; }
SHOW_CURSOR=""
run_setup
}
# First-boot setup up to the owner form, with the screen captured.
setup() {
source "$TMP/provision.sh"
clear_logo() { :; }
sleep() { :; }
say() { printf '%s\n' "$*" >>"$TMP/screen"; }
keyboard_form() {
echo "owner form" >>"$TMP/screen"
exit 0
}
run_setup
}
case $MODE in
provision) provision ;;
attempt) attempt ;;
setup) setup ;;
rekey) luks_rekey "$DEVICE" ;;
accepts) luks_rekey_accepts_password "$DEVICE" ;;
remains) luks_staged_unlock_remains ;;
esac
SH
run() {
local mode=$1 password=$2 crash_at=${3:-0}
echo "$crash_at" >"$tmp/crash-at"
{
ROOT=$ROOT TMP=$tmp BACKEND=$backend DEVICE=$device MODE=$mode PASSWORD=$password CRASH_AT=$crash_at \
OMARCHY_PATH=$runtime OMARCHY_PROC_ROOT=$tmp/$platform/proc OMARCHY_LIFECYCLE_ROOT=$tmp/lifecycle \
PATH="$tmp/$platform/bin:$PATH" bash "$tmp/attempt.sh"
} >>"$tmp/output" 2>&1
}
# The slot the key opens on the volume, or nothing. Tokens never answer.
opens() {
if [[ $backend == "fake" ]]; then
awk -v m="$1" '$2 == m { print $1; exit }' "$tmp/slots"
else
LC_ALL=C cryptsetup open --test-passphrase --verbose --token-type passphrase-only --key-file <(printf '%s' "$1") "$device" 2>&1 |
grep -o 'Key slot [0-9]* unlocked' | grep -o '[0-9]*' || true
fi
}
slot_count() {
if [[ $backend == "fake" ]]; then
wc -l <"$tmp/slots"
else
cryptsetup luksDump "$device" 2>/dev/null | grep -cE '^ +[0-9]+: luks2$|^Key Slot [0-9]+: ENABLED'
fi
}
fixture() {
local format=${1:-luks2}
rm -rf "$tmp/provisioning" "$tmp/etc" "$tmp/boot" "$tmp/log" "$tmp/output" "$tmp/trace" "$tmp/rebuilds" "$tmp/adds" "$tmp/rebuild-fail" "$tmp/kill-noop" \
"$tmp/prepare-fail" "$tmp/screen" "$tmp/stale" "$tmp/token-slot" "$tmp/slot-record" "$tmp/record-fail" "$tmp/accessories"
mkdir -p "$tmp/provisioning"
chmod 755 "$tmp/provisioning"
touch "$tmp/provisioning/pending"
printf '%s' "$staged_key" >"$tmp/provisioning/luks-key"
if [[ $platform == "apple" ]]; then
mkdir -p "$tmp/boot/omarchy" "$tmp/etc/default"
printf '%s' "$staged_key" >"$tmp/boot/omarchy/luks-key"
echo 'GRUB_CMDLINE_LINUX="rd.luks.name=root-uuid=root rd.luks.key=root-uuid=/omarchy/luks-key:UUID=boot-uuid"' >"$tmp/etc/default/grub"
else
mkdir -p "$tmp/etc/omarchy" "$tmp/etc/limine-entry-tool.d" "$tmp/etc/mkinitcpio.conf.d"
printf '%s' "$staged_key" >"$tmp/etc/omarchy/provisioning.key"
echo 'KERNEL_CMDLINE[default]+=" cryptkey=rootfs:/etc/omarchy/provisioning.key"' \
>"$tmp/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf"
echo 'FILES+=(/etc/omarchy/provisioning.key)' >"$tmp/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf"
fi
echo 0 >"$tmp/steps"
: >"$tmp/log"
# A reset leaves the previous owner's slot next to the staged key.
if [[ $backend == "fake" ]]; then
device=$tmp/volume
: >"$device"
printf '0 %s\n1 %s\n' "$staged_key" "$seller_key" >"$tmp/slots"
else
device=$tmp/volume.img
rm -f "$device"
truncate -s 32M "$device"
local pbkdf=(--pbkdf pbkdf2 --pbkdf-force-iterations 1000)
[[ $format == "luks1" ]] && pbkdf=(--pbkdf-force-iterations 1000)
cryptsetup luksFormat -q --type "$format" "${pbkdf[@]}" "$device" <(printf '%s' "$staged_key")
cryptsetup luksAddKey "${pbkdf[@]}" --key-file <(printf '%s' "$staged_key") "$device" <(printf '%s' "$seller_key")
fi
}
# What a previous owner can leave on the real volume: a systemd-tpm2 token and
# a luks2-keyring token on slot $1, the second answered from the session keyring
# with $2. Fails when the kernel keyring is out of reach (no keyctl, or a
# container's seccomp profile), so that a bare cryptsetup open does not take the
# token in place of any key.
enroll_tokens() {
local slot=$1 key=$2 description=omarchy-test-token-$$
command -v keyctl >/dev/null || return 1
token_key=$(printf '%s' "$key" | keyctl padd user "$description" @s 2>/dev/null) || return 1
keyctl timeout "$token_key" 600 >/dev/null 2>&1 || true
printf '{"type":"systemd-tpm2","keyslots":["%s"],"tpm2-blob":"AA==","tpm2-pcrs":[7],"tpm2-pcr-bank":"sha256","tpm2-primary-alg":"ecc","tpm2-policy-hash":"00","tpm2-pin":false}' "$slot" |
cryptsetup token import --disable-external-tokens "$device" || return 1
cryptsetup token add --key-description "$description" --key-slot "$slot" "$device" >/dev/null || return 1
LC_ALL=C cryptsetup open --test-passphrase --verbose --key-file <(printf 'not-a-key') "$device" 2>&1 | grep -qx "Key slot $slot unlocked."
}
unlock_files_present() {
[[ -e $tmp/etc/omarchy/provisioning.key || -e $tmp/etc/limine-entry-tool.d/99-omarchy-provisioning-unlock.conf ||
-e $tmp/etc/mkinitcpio.conf.d/99-omarchy-provisioning-key.conf || -e $tmp/boot/omarchy/luks-key ]] ||
grep -qs 'rd\.luks\.key=' "$tmp/etc/default/grub"
}
no_secrets_in() {
local file
local -a keys=(-e "$staged_key" -e "$seller_key" -e "$owner_password" -e "other-password")
for file in "$@"; do
[[ -e $file ]] || continue
! grep -Fq "${keys[@]}" "$file" ||
fail "$backend: $(basename "$file") holds no key material" "$(cat "$file")"
done
}
# Between attempts the disk must still open at the next boot: unattended while
# the auto-unlock remains, otherwise with a password the owner typed.
assert_recoverable() {
local context=$1 password=$2
if unlock_files_present; then
[[ -n $(opens "$staged_key") ]] || fail "$backend: $context: the auto-unlock key still opens the disk"
else
[[ -n $(opens "$password") ]] || fail "$backend: $context: the owner's password opens the disk once auto-unlock is gone"
fi
if [[ -n $(opens "$staged_key") && ! -f $tmp/provisioning/luks-key ]]; then
fail "$backend: $context: the staged key file is kept while it still unlocks the volume"
fi
if [[ -f $tmp/provisioning/luks-rekey.state ]]; then
[[ $(stat -c %a "$tmp/provisioning/luks-rekey.state") == "600" ]] || fail "$backend: $context: the journal is private"
fi
[[ $(stat -c %a "$tmp/provisioning") == "755" ]] || fail "$backend: $context: the provisioning directory stays readable for the new user"
no_secrets_in "$tmp/provisioning/luks-rekey.state" "$tmp/log"
}
# The re-key finished: the owner's slot is all the volume holds.
assert_finished() {
local context=$1 password=$2 max_adds=${3:-1} adds=0 owner
owner=$(opens "$password")
[[ $(slot_count) == "1" && -n $owner ]] || fail "$backend: $context: only the owner's slot remains" "$(cat "$tmp/log")"
[[ -z $(opens "$staged_key") ]] || fail "$backend: $context: the staged install key no longer unlocks the volume"
[[ -z $(opens "$seller_key") ]] || fail "$backend: $context: the previous owner's key no longer unlocks the volume"
[[ ! -e $tmp/provisioning/luks-key ]] || fail "$backend: $context: the staged key file is destroyed"
! unlock_files_present || fail "$backend: $context: no boot-time auto-unlock remains"
[[ -f $tmp/adds ]] && adds=$(grep -c '^owner' "$tmp/adds" || true)
(( adds <= max_adds )) || fail "$backend: $context: the owner's key is added at most $max_adds time(s)"
! run remains "$password" || fail "$backend: $context: nothing of the staged unlock remains"
if [[ $platform == "apple" ]]; then
[[ $(cat "$tmp/slot-record" 2>/dev/null) == "owner=$owner"$'\n'"recovery=" ]] ||
fail "$backend: $context: the boot package records the owner's slot and no recovery slot" "$(cat "$tmp/slot-record" 2>/dev/null)"
else
[[ ! -e $tmp/slot-record ]] || fail "$backend: $context: x86 records no slots"
fi
no_secrets_in "$tmp/provisioning/luks-rekey.state" "$tmp/log" "$tmp/output" "$tmp/slot-record"
}
# Setup completed: the disk opens with the password the account got, and the
# provisioning state is gone.
assert_provisioned() {
assert_finished "$@"
[[ ! -e $tmp/provisioning/pending && ! -e $tmp/provisioning/luks-rekey.state ]] ||
fail "$backend: $1: setup drops pending and the journal together"
}
backends=(fake)
if command -v cryptsetup >/dev/null; then
backends+=(luks2 luks1)
else
skip "cryptsetup is not installed; skipping the file-backed volume runs"
fi
# The crash matrix runs whole setup attempts on every backend, for x86 and for
# Apple.
matrix=()
for backend in "${backends[@]}"; do
matrix+=("x86 $backend")
[[ " ${platforms[*]} " != *" apple "* ]] || matrix+=("apple $backend")
done
for run_spec in "${matrix[@]}"; do
read -r platform backend <<<"$run_spec"
rm -f "$tmp/mac-boot-ran" "$tmp/limine-ran"
format=$backend
[[ $backend == "fake" ]] && format=luks2
fixture "$format"
run attempt "$owner_password" || fail "$backend: uninterrupted setup completes" "$(cat "$tmp/log" "$tmp/output")"
assert_provisioned "uninterrupted" "$owner_password"
[[ $(cat "$tmp/accessories" 2>/dev/null) == "owner" ]] ||
fail "$backend: accessories are enrolled once, after the staged unlock is gone" "$(cat "$tmp/accessories" 2>/dev/null)"
total_steps=$(cat "$tmp/steps")
(( total_steps >= 11 )) || fail "$backend: every durable step is a crash point" "$(cat "$tmp/trace")"
if [[ $platform == "apple" ]]; then
pass "apple $backend: uninterrupted setup leaves only the owner's slot, records it and destroys the staged key"
else
pass "x86 $backend: uninterrupted setup leaves only the owner's slot and destroys the staged key"
fi
# After each kill the owner reboots and answers the form again, with the same
# password or a new one. The account takes whatever the form accepts, so the
# disk must open with that password when setup finishes.
for (( step = 1; step <= total_steps; step++ )); do
for retry_password in "$owner_password" other-password; do
fixture "$format"
if run attempt "$owner_password" "$step"; then
fail "$backend: setup is killed at step $step"
fi
point=$(sed -n "${step}p" "$tmp/trace")
assert_recoverable "killed after '$point'" "$owner_password"
[[ -e $tmp/provisioning/pending ]] || fail "$backend: killed after '$point', setup runs again"
staged_alive=$([[ -n $(opens "$staged_key") ]] && echo 1 || echo 0)
if run accepts "$retry_password"; then
[[ $retry_password == "$owner_password" ]] || (( staged_alive )) ||
fail "$backend: after '$point' a new password is refused once the staged key is retired"
run attempt "$retry_password" || fail "$backend: after '$point' setup completes" "$(cat "$tmp/log")"
if [[ $retry_password == "$owner_password" ]]; then
assert_provisioned "rerun after '$point'" "$retry_password"
if head -n "$step" "$tmp/trace" | grep -q 'journal phase boot'; then
[[ $(wc -l <"$tmp/rebuilds") == "1" ]] || fail "$backend: rerun after '$point' does not rebuild boot again"
fi
else
assert_provisioned "new password after '$point'" "$retry_password" 2
[[ -z $(opens "$owner_password") ]] || fail "$backend: after '$point' the abandoned password is retired"
fi
else
[[ $retry_password != "$owner_password" ]] || fail "$backend: after '$point' the original password is always accepted"
(( ! staged_alive )) || fail "$backend: after '$point' a new password is accepted while the staged key works"
if run attempt "$retry_password"; then fail "$backend: after '$point' setup refuses a new password"; fi
assert_recoverable "refused new password after '$point'" "$owner_password"
[[ -e $tmp/provisioning/pending ]] || fail "$backend: after '$point' a refused password keeps setup pending"
run attempt "$owner_password" || fail "$backend: after '$point' the original password completes" "$(cat "$tmp/log")"
assert_provisioned "original password after '$point'" "$owner_password"
fi
done
done
if [[ $platform == "apple" ]]; then
pass "apple $backend: killed after each of $total_steps steps, setup resumes to a disk that opens with the account's password, with its slot recorded"
else
pass "x86 $backend: killed after each of $total_steps steps, setup resumes to a disk that opens with the account's password"
fi
if [[ $platform == "x86" ]]; then
[[ ! -e $tmp/mac-boot-ran ]] || fail "x86 $backend: no Mac boot entrypoint runs" "$(cat "$tmp/mac-boot-ran")"
pass "x86 $backend: dispatch is a no-op and the Limine UKI path runs unchanged"
else
[[ ! -e $tmp/limine-ran ]] || fail "apple $backend: the Limine UKI path never runs on Apple"
grep -qx provision-commit "$tmp/mac-boot-ran" && grep -qx provision-verify "$tmp/mac-boot-ran" ||
fail "apple $backend: the boot package commits and verifies the unlock"
pass "apple $backend: the boot package owns the boot-time unlock through dispatch"
fi
done
platform=x86
backend=fake
fixture
touch "$tmp/rebuild-fail"
if run rekey "$owner_password"; then fail "a failed boot rebuild fails the attempt"; fi
unlock_files_present || fail "a failed boot rebuild restores the auto-unlock"
[[ $(cat "$tmp/etc/omarchy/provisioning.key") == "$staged_key" ]] || fail "the restored keyfile is the staged key"
[[ -n $(opens "$staged_key") && -n $(opens "$seller_key") ]] || fail "a failed boot rebuild retires no slot"
rm "$tmp/rebuild-fail"
run rekey "$owner_password" || fail "the retry after a failed rebuild completes" "$(cat "$tmp/log")"
assert_finished "retry after a failed rebuild" "$owner_password"
pass "a failed boot rebuild keeps the unattended unlock and every slot for the retry"
fixture
printf '2 %s\n' tpm-sealed-key >>"$tmp/slots"
echo 2 >"$tmp/token-slot"
run provision "$owner_password" || fail "setup completes beside an enrolled token" "$(cat "$tmp/log")"
assert_provisioned "beside the previous owner's token" "$owner_password"
pass "a token the previous owner enrolled never answers for a key and is retired with its slot"
if [[ " ${backends[*]} " == *" luks2 "* ]]; then
backend=luks2
fixture luks2
if enroll_tokens 1 "$seller_key"; then
run provision "$owner_password" || fail "luks2: setup completes beside live tokens" "$(cat "$tmp/log" "$tmp/output")"
assert_provisioned "beside the previous owner's live tokens" "$owner_password"
pass "luks2: with a live keyring token and a TPM2 token on the previous owner's slot, only real keys count"
else
skip "the kernel keyring is out of reach; skipping the live token run"
fi
backend=fake
fi
fixture
touch "$tmp/kill-noop"
if run rekey "$owner_password"; then fail "a slot kill that changes nothing fails the re-key"; fi
[[ -f $tmp/provisioning/luks-key && -f $tmp/provisioning/luks-rekey.state ]] || fail "an unverified retirement keeps the staged key and journal"
rm "$tmp/kill-noop"
run rekey "$owner_password" || fail "the retry after an ineffective slot kill completes" "$(cat "$tmp/log")"
assert_finished "retry after an ineffective slot kill" "$owner_password"
pass "the staged key is destroyed only after the volume proves it no longer opens"
fixture
printf '0 %s\n1 %s\n2 %s\n' "$staged_key" "$seller_key" "$owner_password" >"$tmp/slots"
printf 'staged_slot=0\nowner_slot=2\nphase=boot\n' >"$tmp/provisioning/luks-rekey.state"
run rekey "$owner_password" || fail "a leftover auto-unlock after the boot step is removed" "$(cat "$tmp/log")"
assert_finished "leftover auto-unlock after the boot step" "$owner_password"
[[ $(wc -l <"$tmp/rebuilds") == "1" ]] || fail "the leftover auto-unlock triggers a boot rebuild"
pass "an auto-unlock that outlives the boot step is rebuilt away rather than blocking forever"
# The boot files ask for the disk password in the layout vconsole.conf held
# when they were built. A retry after the owner picked another layout rebuilds
# them once, at the boot step and after done; the same layout rebuilds nothing,
# and a journal from before the fingerprint rebuilds once.
choose_layout() {
mkdir -p "$tmp/etc"
printf 'KEYMAP=%s\nXKBLAYOUT=%s\nXKBMODEL=pc105\n' "$1" "$2" >"$tmp/etc/vconsole.conf"
}
rebuild_count() {
wc -l <"$tmp/rebuilds" 2>/dev/null || echo 0
}
assert_fingerprint() {
local context=$1 journal=$tmp/provisioning/luks-rekey.state
grep -Eqx 'boot_layout=[0-9a-f]{64}' "$journal" || fail "$context: the journal records the layout as a hash" "$(cat "$journal")"
! grep -Eqw 'dk|us' "$journal" "$tmp/log" "$tmp/trace" || fail "$context: the journal and log never name the layout"
}
boot_layout_runs() {
local name=$1 stop step refresh before
fixture
choose_layout us us
run rekey "$owner_password" || fail "$name: the re-key completes with a layout" "$(cat "$tmp/log")"
assert_fingerprint "$name: uninterrupted"
cp "$tmp/trace" "$tmp/layout-trace"
for stop in boot done; do
step=$(grep -m1 "^[0-9]* journal phase $stop" "$tmp/layout-trace" | cut -d' ' -f1)
[[ -n $step ]] || fail "$name: the trace names the $stop journal write" "$(cat "$tmp/layout-trace")"
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
[[ $(rebuild_count) == 1 ]] || fail "$name: killed at $stop after one rebuild"
run rekey "$owner_password" || fail "$name: the same layout completes after $stop" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 1 ]] || fail "$name: an unchanged layout after $stop rebuilds nothing"
assert_finished "$name: unchanged layout after $stop" "$owner_password"
# The owner picks another layout on the retry right after the interruption.
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
before=$(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state")
choose_layout dk dk
run rekey "$owner_password" || fail "$name: a changed layout after $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 2 ]] || fail "$name: a changed layout after $stop rebuilds the boot files"
[[ $(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state") != "$before" ]] ||
fail "$name: the journal takes the new layout's fingerprint after $stop"
[[ $(grep -c '^[0-9]* journal boot_layout ' "$tmp/trace") == 1 && $(grep -c "^[0-9]* journal phase $stop" "$tmp/trace") == 1 ]] ||
fail "$name: the rebuild after $stop records only the fingerprint, keeping the phase" "$(cat "$tmp/trace")"
refresh=$(awk -v s="$step" '$1 > s && / boot rebuilt$/ { print $1; exit }' "$tmp/trace")
run rekey "$owner_password" || fail "$name: a rerun after the rebuild completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 2 ]] || fail "$name: the changed layout after $stop rebuilds only once"
assert_finished "$name: changed layout after $stop" "$owner_password"
assert_fingerprint "$name: changed layout after $stop"
# Killed between that rebuild and its fingerprint: the retry rebuilds again.
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
choose_layout dk dk
if run rekey "$owner_password" "$refresh"; then fail "$name: the re-key is killed after the layout rebuild at $stop"; fi
[[ $(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state") == "$before" ]] ||
fail "$name: a layout rebuild killed before its journal write keeps the old fingerprint"
run rekey "$owner_password" || fail "$name: the retry after a killed layout rebuild at $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 3 ]] || fail "$name: a layout rebuild killed at $stop runs again"
assert_finished "$name: killed layout rebuild at $stop" "$owner_password"
assert_fingerprint "$name: killed layout rebuild at $stop"
done
# Interrupted at the boot step, the owner picks another layout and a new
# password while the staged key still opens the disk.
step=$(grep -m1 "^[0-9]* journal phase boot" "$tmp/layout-trace" | cut -d' ' -f1)
fixture
choose_layout us us
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the boot journal write"; fi
choose_layout dk dk
run rekey other-password || fail "$name: a new layout and password after the boot step complete" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 2 ]] || fail "$name: a new layout and password after the boot step rebuild once"
assert_finished "$name: new layout and password after the boot step" other-password 2
assert_fingerprint "$name: new layout and password after the boot step"
# A journal from before the fingerprint, at the boot step with the auto-unlock
# already gone and after done: rebuilt once, then recorded.
for stop in boot done; do
fixture
choose_layout dk dk
run rekey "$owner_password" || fail "$name: the re-key completes" "$(cat "$tmp/log")"
step=$(grep -m1 -n "^[0-9]* journal phase $stop" "$tmp/trace" | cut -d: -f1)
fixture
choose_layout dk dk
if run rekey "$owner_password" "$step"; then fail "$name: the re-key is killed after the $stop journal write"; fi
sed -i '/^boot_layout=/d' "$tmp/provisioning/luks-rekey.state"
rm -f "$tmp/rebuilds"
run rekey "$owner_password" || fail "$name: an older journal at $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 1 ]] || fail "$name: an older journal at $stop rebuilds the boot files once"
run rekey "$owner_password" || fail "$name: a rerun of an older journal at $stop completes" "$(cat "$tmp/log")"
[[ $(rebuild_count) == 1 ]] || fail "$name: an older journal at $stop rebuilds only once"
assert_finished "$name: older journal at $stop" "$owner_password"
assert_fingerprint "$name: older journal at $stop"
done
# After done a failed rebuild keeps the old fingerprint, so the retry rebuilds.
fixture
choose_layout us us
run rekey "$owner_password" || fail "$name: the re-key completes" "$(cat "$tmp/log")"
before=$(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state")
choose_layout dk dk
touch "$tmp/rebuild-fail"
if run rekey "$owner_password"; then fail "$name: a failed layout rebuild after done fails the attempt"; fi
[[ $(grep -m1 '^boot_layout=' "$tmp/provisioning/luks-rekey.state") == "$before" ]] ||
fail "$name: a failed layout rebuild keeps the old fingerprint"
rm "$tmp/rebuild-fail"
run rekey "$owner_password" || fail "$name: the retry after a failed layout rebuild completes" "$(cat "$tmp/log")"
assert_finished "$name: retry after a failed layout rebuild" "$owner_password"
assert_fingerprint "$name: retry after a failed layout rebuild"
}
boot_layout_runs x86
pass "x86: a changed keyboard layout rebuilds the boot files once, an unchanged one never, and an older journal once"
fixture
printf 'wrong-key' >"$tmp/provisioning/luks-key"
if run rekey "$owner_password"; then fail "a staged key that opens nothing fails the re-key"; fi
[[ ! -e $tmp/provisioning/luks-rekey.state && $(slot_count) == "2" ]] || fail "a dead staged key changes nothing"
pass "a fresh re-key refuses a staged key that no longer unlocks the volume"
fixture
rm "$tmp/provisioning/luks-key"
run remains "$owner_password" || fail "a leftover auto-unlock counts as the staged unlock remaining"
if run rekey "$owner_password"; then fail "a leftover auto-unlock without its staged key fails closed"; fi
unlock_files_present || fail "the leftover auto-unlock is not silently accepted"
pass "a boot-time auto-unlock without its staged key keeps provisioning from finishing"
fixture
if run rekey "$staged_key"; then fail "the staged key is refused as the owner's password"; fi
[[ $(slot_count) == "2" ]] && unlock_files_present || fail "refusing the staged key as a password changes nothing"
run rekey "$owner_password" || fail "a retry with a real password completes" "$(cat "$tmp/log")"
assert_finished "retry after refusing the staged key" "$owner_password"
pass "the owner's password must differ from the staged install key"
fixture
printf 'phase=owner\nstaged_slot=0\nowner_slot=2\nrecovery_slot=5\n' >"$tmp/provisioning/luks-rekey.state"
chmod 600 "$tmp/provisioning/luks-rekey.state"
(
source "$ROOT/install/provisioning/luks-rekey.sh"
REKEY_STATE=$tmp/provisioning/luks-rekey.state
rekey_state_put phase boot
)
[[ $(grep -c . "$tmp/provisioning/luks-rekey.state") == "4" ]] && grep -qx 'phase=boot' "$tmp/provisioning/luks-rekey.state" &&
grep -qx 'recovery_slot=5' "$tmp/provisioning/luks-rekey.state" || fail "a journal write keeps every other recorded key" "$(cat "$tmp/provisioning/luks-rekey.state")"
pass "journal writes replace only their own keys"
fixture
rm "$tmp/provisioning/luks-key"
if run provision "$owner_password"; then fail "provisioning with a leftover auto-unlock and no staged key fails"; fi
[[ -e $tmp/provisioning/pending ]] && unlock_files_present || fail "provisioning keeps its state while the auto-unlock remains"
pass "first-boot provisioning never finishes with the boot-time auto-unlock still configured"
fixture
rm -rf "$tmp/provisioning/luks-key" "$tmp/etc"
run provision "$owner_password" || fail "unencrypted provisioning finishes" "$(cat "$tmp/log" "$tmp/output")"
[[ ! -e $tmp/provisioning/pending && ! -e $tmp/provisioning/luks-rekey.state && $(slot_count) == "2" ]] ||
fail "unencrypted provisioning skips the re-key"
pass "provisioning without a staged key or auto-unlock skips the re-key"
if [[ " ${platforms[*]} " == *" apple "* ]]; then
# Apple: a failed boot-package commit keeps the unattended unlock and every slot.
platform=apple
fixture
touch "$tmp/rebuild-fail"
if run rekey "$owner_password"; then fail "apple: a failed boot-package commit fails the attempt"; fi
unlock_files_present || fail "apple: a failed commit keeps the boot-time unlock"
[[ -n $(opens "$staged_key") && -n $(opens "$seller_key") ]] || fail "apple: a failed commit retires no slot"
rm "$tmp/rebuild-fail"
run rekey "$owner_password" || fail "apple: the retry after a failed commit completes" "$(cat "$tmp/log")"
assert_finished "apple: retry after a failed commit" "$owner_password"
pass "apple: a failed boot-package commit keeps the unattended unlock and every slot for the retry"
# Apple: an inherited decision never replaces resolving who owns the unlock.
fixture
rm -f "$tmp/limine-ran"
export UNLOCK_OWNER=limine
run provision "$owner_password" || fail "apple: setup completes with UNLOCK_OWNER in its environment" "$(cat "$tmp/log")"
unset UNLOCK_OWNER
assert_provisioned "apple: UNLOCK_OWNER inherited" "$owner_password"
[[ ! -e $tmp/limine-ran ]] || fail "apple: an inherited UNLOCK_OWNER does not select the Limine path"
pass "apple: an inherited UNLOCK_OWNER is ignored"
# Apple: the boot package's commit rebuilds for a changed layout, after done too.
boot_layout_runs apple
pass "apple: a changed keyboard layout reruns the boot package's commit once, an unchanged one never, and an older journal once"
fi
# After a factory reset left entries for another machine identity, the menu
# starts over and limine-update rebuilds, on Apple too, and setup still
# finishes only once the boot package verifies nothing of the staged unlock
# remains.
for platform in "${platforms[@]}"; do
fixture
rm -rf "$tmp/provisioning/luks-key" "$tmp/etc" "$tmp/boot" "$tmp/limine-ran" "$tmp/mac-boot-ran"
touch "$tmp/stale"
run provision "$owner_password" || fail "$platform: stale boot entries are refreshed" "$(cat "$tmp/log" "$tmp/output")"
[[ ! -e $tmp/provisioning/pending && $(cat "$tmp/limine-ran") == $'reset\nupdate' ]] ||
fail "$platform: the Limine menu is reset and limine-update rebuilds" "$(cat "$tmp/limine-ran")"
if [[ $platform == "x86" ]]; then
[[ ! -e $tmp/mac-boot-ran ]] || fail "x86: no Mac entrypoint runs"
else
grep -qx provision-verify "$tmp/mac-boot-ran" || fail "apple: the boot package still has the last word"
fi
done
if [[ " ${platforms[*]} " == *" apple "* ]]; then
platform=apple
fixture
rm -rf "$tmp/provisioning/luks-key" "$tmp/limine-ran"
touch "$tmp/stale"
if run provision "$owner_password"; then fail "apple: a leftover boot-partition unlock still stops setup after the refresh"; fi
[[ -e $tmp/provisioning/pending ]] || fail "apple: setup stays pending"
fi
pass "stale boot entries are rebuilt by limine-update, behind the boot package's verify on Apple"
# Before the owner form: a no-op on x86 even with Mac entrypoints on disk, the
# boot package's own answer on Apple.
platform=x86
fixture
rm -f "$tmp/mac-boot-ran"
touch "$tmp/prepare-fail"
run setup "$owner_password" || fail "x86: setup reaches the owner form without a boot package" "$(cat "$tmp/output")"
[[ $(cat "$tmp/screen") == "owner form" && ! -e $tmp/mac-boot-ran ]] ||
fail "x86: no Mac boot entrypoint runs before the owner form" "$(cat "$tmp/screen")"
pass "x86: the platform check before the owner form is a no-op"
if [[ " ${platforms[*]} " == *" apple "* ]]; then
platform=apple
fixture
run setup "$owner_password" && grep -qx 'owner form' "$tmp/screen" ||
fail "apple: setup reaches the owner form with the boot package ready" "$(cat "$tmp/output")"
fixture
touch "$tmp/prepare-fail"
if run setup "$owner_password"; then fail "apple: setup stops when the boot package is not ready"; fi
! grep -qx 'owner form' "$tmp/screen" || fail "apple: the owner is asked nothing when the boot package is not ready"
grep -q 'The boot partition is not mounted.' "$tmp/screen" && grep -q 'The boot partition is not mounted.' "$tmp/log" ||
fail "apple: the boot package's reason reaches the screen and the log" "$(cat "$tmp/screen" "$tmp/log" 2>/dev/null)"
pass "apple: setup stops before the owner form when the boot package is not ready"
# Apple without omarchy-mac-boot, or with one older than its provisioning
# entrypoints: setup stops before the owner form naming the package, and a
# worker that got past it anyway never finishes while any part of the staged
# unlock remains.
mv "$mac_boot" "$tmp/mac-boot.off"
for package in missing old; do
# An older package can already ship other files in that directory.
if [[ $package == old ]]; then
install -d -m 755 "$mac_boot"
install -m 755 /dev/null "$mac_boot/boot-hook"
fi
fixture
rm -f "$tmp/limine-ran" "$tmp/mac-boot-ran"
if run setup "$owner_password"; then fail "apple ($package package): setup refuses without provisioning entrypoints"; fi
! grep -qx 'owner form' "$tmp/screen" || fail "apple ($package package): the owner is asked nothing"
grep -q 'provision-prepare on aarch64-apple needs omarchy-mac-boot' "$tmp/screen" ||
fail "apple ($package package): the boot package is named on the screen" "$(cat "$tmp/screen" 2>/dev/null)"
grep -q '/usr/lib/omarchy/mac-boot/provision-prepare' "$tmp/log" || fail "apple ($package package): the log names the entrypoint" "$(cat "$tmp/log")"
if run provision "$owner_password"; then fail "apple ($package package): provisioning fails"; fi
[[ -e $tmp/provisioning/pending && -f $tmp/provisioning/luks-key ]] && unlock_files_present ||
fail "apple ($package package): provisioning keeps its state and the staged unlock"
[[ -n $(opens "$staged_key") && -n $(opens "$seller_key") ]] || fail "apple ($package package): no slot is retired"
fixture
rm "$tmp/provisioning/luks-key"
if run provision "$owner_password"; then fail "apple ($package package): a leftover boot-partition unlock fails provisioning"; fi
[[ -e $tmp/provisioning/pending ]] && unlock_files_present || fail "apple ($package package): a leftover boot-partition unlock keeps provisioning pending"
[[ ! -e $tmp/limine-ran && ! -e $tmp/mac-boot-ran ]] || fail "apple ($package package): the Limine UKI path is no fallback"
[[ $package == missing ]] || rm -r "$mac_boot"
done
mv "$tmp/mac-boot.off" "$mac_boot"
pass "apple: without omarchy-mac-boot's provisioning entrypoints setup stops naming the package, and the worker fails closed"
fi
# A boot package that implements only one of the commit/verify pair owns
# nothing: provisioning fails closed rather than mixing it with the Limine path.
mkdir -p "$tmp/half/bin"
cat >"$tmp/half/bin/omarchy-lifecycle-dispatch" <<SH
#!/bin/bash
echo "\$*" >>"$tmp/half-ran"
[[ \$* != "--resolve provision-commit" ]] || echo /usr/lib/omarchy/mac-boot/provision-commit
SH
chmod +x "$tmp/half/bin/omarchy-lifecycle-dispatch"
runtime=$tmp/half
fixture
rm -f "$tmp/limine-ran" "$tmp/half-ran"
if run provision "$owner_password"; then fail "a half-implemented unlock fails provisioning"; fi
[[ -e $tmp/provisioning/pending ]] && unlock_files_present || fail "a half-implemented unlock keeps provisioning pending"
[[ -s $tmp/half-ran && ! -e $tmp/limine-ran ]] && ! grep -qv -- '--resolve' "$tmp/half-ran" ||
fail "a half-implemented unlock runs neither the platform nor the Limine path" "$(cat "$tmp/half-ran")"
runtime=$ROOT
pass "a boot package implementing only half of the unlock pair fails closed"
# Any slot but the owner's is retired, a recovery key an earlier setup added
# and recorded in the journal included.
platform=x86
backend=fake
fixture
printf '2 recovery-key\n' >>"$tmp/slots"
printf 'recovery_slot=2\nrecovery_shown=1\n' >"$tmp/provisioning/luks-rekey.state"
chmod 600 "$tmp/provisioning/luks-rekey.state"
run rekey "$owner_password" || fail "the re-key beside an earlier recovery slot completes" "$(cat "$tmp/log")"
[[ -z $(opens "$staged_key") && -z $(opens "$seller_key") && -z $(opens recovery-key) && -n $(opens "$owner_password") && $(slot_count) == 1 ]] ||
fail "every slot but the owner's is retired" "$(cat "$tmp/slots")"
pass "the re-key retires every slot but the owner's, an earlier recovery slot included"
@@ -0,0 +1,155 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# A platform package names keys the way its keyboards print them. On a MacBook
# XF86MonBrightnessUp/Down are the F2/F1 keys, so without the names the menu
# shows rows like "SHIFT + XF86MonBrightnessUp". A cache written before the
# names were installed must not win over them.
mock_bin=$(mktemp -d)
cache_dir=$(mktemp -d)
platform=$(mktemp -d)
cleanup() {
rm -rf "$mock_bin" "$cache_dir" "$platform"
}
trap cleanup EXIT
cat >"$mock_bin/hyprctl" <<'SH'
#!/bin/bash
if [[ $1 == "devices" ]]; then
echo "Keyboards:"
echo " active keymap: Apple Keyboard (apple_vndr)"
exit 0
fi
cat <<'BINDS'
bindl
modmask: 1
key: XF86MonBrightnessUp
keycode: 0
description: Keyboard brightness up
dispatcher: exec
arg: omarchy-brightness-keyboard up
bindl
modmask: 1
key: XF86MonBrightnessDown
keycode: 0
description: Keyboard brightness down
dispatcher: exec
arg: omarchy-brightness-keyboard down
bindi
modmask: 8
key: XF86MonBrightnessUp
keycode: 0
description: Brightness up precise
dispatcher: exec
arg: omarchy-brightness-display +1%
bind
modmask: 64
key: F5
keycode: 0
description: Press brightness up
dispatcher: exec
arg: wtype -k XF86MonBrightnessUp
bindm
modmask: 64
key: Q
keycode: 0
description: Close window
dispatcher: killactive
arg:
BINDS
SH
chmod +x "$mock_bin/hyprctl"
# Minimal valid keymap so keycode resolution has something to chew on.
cat >"$mock_bin/xkbcli" <<'SH'
#!/bin/bash
cat <<'KEYMAP'
xkb_keycodes {
<AD01> = 24;
};
xkb_symbols {
key <AD01> { [ q ] };
};
KEYMAP
SH
chmod +x "$mock_bin/xkbcli"
cat >"$mock_bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
exit 1
SH
chmod +x "$mock_bin/omarchy-cmd-present"
# The menu reads key names from the fixed platform root; copies of it read a
# fixture root with key names, and one without.
mkdir -p "$platform/root" "$platform/bin"
printf '%s\n' 'XF86MonBrightnessUp F2' 'XF86MonBrightnessDown F1' 'not a line' 'Bad-Sym F3' >"$platform/root/key-names"
platform_root_copy "$ROOT/bin/omarchy-menu-keybindings" "$platform/bin/with-names" "$platform/root"
platform_root_copy "$ROOT/bin/omarchy-menu-keybindings" "$platform/bin/without-names" "$platform/none"
# Pre-seed the exact cache file that v13, which knew no key names, would read
# for these mocked inputs. Cached records use the rendered row as field 1, followed by
# the dispatcher and argument fields.
mkdir -p "$cache_dir/omarchy"
v13_key=$({
printf 'v13\n'
"$mock_bin/hyprctl" devices 2>/dev/null | grep -F 'active keymap:'
"$mock_bin/hyprctl" binds 2>/dev/null
} | sha256sum | awk '{ print $1 }')
cat >"$cache_dir/omarchy/keybindings-$v13_key.records" <<'EOF'
SHIFT + XF86MonBrightnessUp → Keyboard brightness up exec omarchy-brightness-keyboard up
SHIFT + XF86MonBrightnessDown → Keyboard brightness down exec omarchy-brightness-keyboard down
ALT + XF86MonBrightnessUp → Brightness up precise exec omarchy-brightness-display +1%
SUPER + Q → Close window killactive
EOF
output=$(PATH="$mock_bin:$PATH" XDG_CACHE_HOME="$cache_dir" "$platform/bin/with-names" --print)
tr -s ' ' <<<"$output" | grep -qF 'SHIFT + F2 → Keyboard brightness up' || \
fail "SHIFT + XF86MonBrightnessUp renders as SHIFT + F2 with its description" "$output"
tr -s ' ' <<<"$output" | grep -qF 'SHIFT + F1 → Keyboard brightness down' || \
fail "SHIFT + XF86MonBrightnessDown renders as SHIFT + F1 with its description" "$output"
tr -s ' ' <<<"$output" | grep -qF 'ALT + F2 → Brightness up precise' || \
fail "modifier combos keep the F-key name (ALT + F2)" "$output"
grep -qF 'XF86MonBrightness' <<<"$output" && \
fail "raw XF86MonBrightness symbols no longer leak into the menu" "$output"
tr -s ' ' <<<"$output" | grep -qF 'SUPER + Q → Close window' || \
fail "unrelated bindings render unchanged (SUPER + Q)" "$output"
# The menu runs a chosen row from its cached record, which carries what the
# binding runs.
records=$(cat "$cache_dir"/omarchy/keybindings-*.records)
grep -qF 'wtype -k XF86MonBrightnessUp' <<<"$records" && ! grep -qF 'wtype -k F2' <<<"$records" ||
fail "a key name never changes what a binding runs" "$records"
pass "keybindings menu shows a platform's key names (F1/F2), stale caches ignored"
# A key-names file saved with CRLF line ends, or with blanks around a line,
# reads the same, as displays.conf does.
mkdir -p "$platform/crlf"
printf '%s\r\n' 'XF86MonBrightnessUp F2' ' XF86MonBrightnessDown F1 ' >"$platform/crlf/key-names"
platform_root_copy "$ROOT/bin/omarchy-menu-keybindings" "$platform/bin/crlf-names" "$platform/crlf"
output=$(PATH="$mock_bin:$PATH" XDG_CACHE_HOME="$cache_dir" "$platform/bin/crlf-names" --print)
tr -s ' ' <<<"$output" | grep -qF 'SHIFT + F2 → Keyboard brightness up' && tr -s ' ' <<<"$output" | grep -qF 'SHIFT + F1 → Keyboard brightness down' ||
fail "key names saved with CRLF line ends and blanks around them still apply" "$output"
pass "keybindings menu reads key names saved with CRLF line ends"
# Without a platform package the keys keep their own names, and a cache made
# with the names is not reused.
output=$(PATH="$mock_bin:$PATH" XDG_CACHE_HOME="$cache_dir" "$platform/bin/without-names" --print)
tr -s ' ' <<<"$output" | grep -qF 'SHIFT + XF86MonBrightnessUp → Keyboard brightness up' ||
fail "without key names the brightness key keeps its own name" "$output"
grep -qF 'SHIFT + F2' <<<"$output" && fail "without key names nothing is renamed F2" "$output"
pass "keybindings menu keeps keysym names without a platform package"
+26 -5
View File
@@ -286,18 +286,27 @@ assert(
assert(!defaultById['install.ai.crush'], 'menu removes Crush from Install > AI')
// Software you already have keeps its place in Install, dimmed rather than
// dropped, so the list reads as a catalog of what Omarchy can install.
// Chromium Account is the sole Install row with anything left to hide for, so
// any other `when:` here is a row that went back to vanishing once installed.
// Chromium Account is the sole Install row with anything left to hide for. The
// Windows VM, whose guest can't run elsewhere, and the installers whose vendors
// ship Linux builds for x86_64 alone hide only off x86_64, so any other `when:`
// here is a row that went back to vanishing once installed.
const windowsGuard = 'omarchy-hw-x86'
const x86OnlyInstalls = ['install.windows', 'install.browser.edge', 'install.service.dropbox', 'install.service.spotify', 'install.gaming.minecraft', 'install.gaming.heroic']
assertDeepEqual(
defaultItems
.filter(item => item.id.startsWith('install.') && item.action && item.when)
.map(item => item.id),
['install.service.chromium-account'],
.map(item => item.id)
.sort(),
[...x86OnlyInstalls, 'install.service.chromium-account'].sort(),
'menu never hides an Install row because the software is already there'
)
assert(
x86OnlyInstalls.every(id => defaultById[id].when === windowsGuard),
'menu hides the Windows VM and the x86_64-only installers only off x86_64'
)
assert(
['install.browser.zen', 'install.editor.vscode', 'install.gaming.steam', 'install.development.rust', 'install.windows'].every(
id => defaultById[id].disabled && !defaultById[id].when
id => defaultById[id].disabled && (!defaultById[id].when || defaultById[id].when === windowsGuard)
),
'menu dims the Install rows for software that is already installed'
)
@@ -343,6 +352,13 @@ assert(
defaultById['setup.security.passwordless-sudo'].action.includes('omarchy-sudo-passwordless'),
'menu places Passwordless Sudo under Setup > Security'
)
assert(
defaultById['setup.security.usb-authorization'].action.includes('omarchy-setup-security-usb-authorization')
&& defaultById['remove.security.usb-authorization'].action.includes('omarchy-remove-security-usb-authorization')
&& defaultById['setup.security.usb-authorization-boot'].action.includes('omarchy-setup-security-usb-authorization --boot')
&& defaultById['remove.security.usb-authorization-boot'].action.includes('omarchy-remove-security-usb-authorization --boot-only'),
'menu can enable and remove USB device authorization under Security'
)
assert(
!defaultById['trigger.toggle.direct-boot'] && !defaultById['trigger.toggle.passwordless-sudo'],
'menu removes the relocated toggles from Trigger > Toggle'
@@ -467,6 +483,11 @@ assertEqual(
'omarchy-hw-webcam',
'menu only shows webcam screen recording when a webcam is available'
)
assertEqual(
defaultById['trigger.capture.screenrecord.stop'].when,
'omarchy-capture-screenrecording --status',
'menu shows Stop Screenrecording only when its stop has a recording to end'
)
assert(
/font\.family: row\.iconFont\.length > 0 \? row\.iconFont : root\.fontFamily/.test(menuQml),
'menu rows support per-icon font families'
+76
View File
@@ -0,0 +1,76 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Apple Silicon's boot package ships mkinitcpio drop-ins 90- to 94- that sort
# after the HOOKS baseline and add the Asahi firmware and systemd unlock hooks
# to it. The fixtures are the drop-ins omarchy-mac-boot 20260926-1.45 installs,
# as read from an encrypted M1 Pro, byte for byte the same as
# omarchy-mac-boot/files/etc/mkinitcpio.conf.d in omacom/omarchy-mac-pkgs at
# 4399105 (MIT). The encryption drop-in leaves a line carrying
# busybox encrypt alone, since that is a Mac unlocked through cryptdevice=, so
# the baseline must give a Mac the systemd line.
require_platform_fixtures "the Apple boot drop-ins on the HOOKS baseline"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
fake_platform "$tmp/platform" aarch64-apple
# No kernel modules here: the HID drop-in adds only what modinfo finds.
printf '#!/bin/bash\nexit 1\n' >"$tmp/platform/bin/modinfo"
chmod +x "$tmp/platform/bin/modinfo"
# $1 is the HOOKS line in mkinitcpio.conf. The drop-ins read /etc/vconsole.conf,
# so point them at a fixture.
compose() {
local config=$tmp/buildconfig conf
local -a conf_files=()
rm -rf "$tmp/conf.d"
mkdir -p "$tmp/conf.d"
cp "$ROOT"/etc/mkinitcpio.conf.d/*.conf "$ROOT"/test/shell.d/fixtures/omarchy-mac-boot-mkinitcpio/*.conf "$tmp/conf.d/"
sed -i "s|/etc/vconsole.conf|$tmp/vconsole.conf|g" "$tmp/conf.d"/*.conf
printf 'MODULES=()\nBINARIES=()\nFILES=()\nHOOKS=(%s)\n' "$1" >"$config"
mapfile -d '' conf_files < <(LC_ALL=C.UTF-8 find "$tmp/conf.d" -maxdepth 1 -xtype f -name '*.conf' -print0 |
sed -z 's/.*\///' | LC_ALL=C.UTF-8 sort -zVu)
for conf in "${conf_files[@]}"; do
cat -- "$tmp/conf.d/$conf" >>"$config"
done
OMARCHY_PROC_ROOT="$tmp/platform/proc" PATH="$tmp/platform/bin:$ROOT/bin:$PATH" "$BASH" -c '
. "$1" || exit 1
printf "HOOKS=%s\nFILES=%s\n" "${HOOKS[*]}" "${FILES[*]}"
' -- "$config"
}
assert_hooks() {
local description="$1" stock="$2" expected="$3" out
out=$(compose "$stock") || fail "$description" "the drop-ins do not source cleanly"
[[ $(sed -n 's/^HOOKS=//p' <<<"$out") == "$expected" ]] ||
fail "$description" "expected: HOOKS=$expected"$'\n'"actual: $out"
pass "$description"
}
# The HOOKS the M1 Pro builds its encrypted image from today.
apple="base systemd plymouth autodetect microcode modconf kms keyboard sd-vconsole block asahi omarchy-vendorfw omarchy-mac-encrypt sd-encrypt filesystems fsck"
alarm_systemd="base systemd autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck"
alarm_busybox="base udev autodetect microcode modconf kms keyboard keymap consolefont block filesystems fsck"
printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$tmp/vconsole.conf"
assert_hooks "a Mac keeps its firmware and systemd unlock hooks" "$alarm_systemd" "$apple"
assert_hooks "a Mac whose mkinitcpio.conf has the busybox line gets the same hooks" "$alarm_busybox" "$apple"
out=$(compose "$alarm_systemd")
[[ $(sed -n 's/^FILES=//p' <<<"$out") == *"$tmp/vconsole.conf"* ]] ||
fail "a Latin layout reaches the image" "$out"
pass "a Latin layout reaches the image"
printf 'KEYMAP=ru\nXKBLAYOUT=ru\n' >"$tmp/vconsole.conf"
assert_hooks "a non-Latin layout keeps the prompt on the US map" "$alarm_systemd" "${apple/ sd-vconsole / }"
out=$(compose "$alarm_systemd")
[[ $(sed -n 's/^FILES=//p' <<<"$out") != *vconsole* ]] ||
fail "a non-Latin layout stays out of the image" "$out"
pass "a non-Latin layout stays out of the image"
# A Mac that unlocks through cryptdevice= keeps its busybox line, drop-ins and all.
printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$tmp/vconsole.conf"
legacy="base udev plymouth autodetect microcode modconf kms keyboard keymap consolefont block encrypt asahi filesystems fsck"
assert_hooks "a busybox encrypt Mac keeps its own line" "$legacy" "$legacy"
+264
View File
@@ -0,0 +1,264 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_platform_fixtures "the composed mkinitcpio HOOKS"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
for platform in aarch64-apple aarch64 x86; do
fake_platform "$test_tmp/platforms/$platform" "$platform"
done
# omarchy-settings without the runtime package: no detector on PATH.
mkdir -p "$test_tmp/platforms/no-detector/bin"
# A device tree naming both Apple and Qualcomm, which the detector refuses.
fake_platform "$test_tmp/platforms/contradiction" aarch64-apple
printf '%s\0' apple,j416c qcom,x1e80100 >"$test_tmp/platforms/contradiction/proc/device-tree/compatible"
etc="$test_tmp/etc"
devices="$test_tmp/devices"
omarchy_hooks="base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt filesystems fsck btrfs-overlayfs"
omarchy_hooks_without_kms=${omarchy_hooks/ kms / }
apple_hooks="base systemd plymouth autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck"
nvidia_modules="nvidia nvidia_modeset nvidia_uvm nvidia_drm"
# The kernel being built, as modinfo answers for it: KERNEL_MODULES names its
# modules (thunderbolt, as on x86 and Aurora, unless a case says otherwise) and
# KERNEL_BUILTINS the ones built in.
cat >"$test_tmp/modinfo" <<'SH'
#!/bin/bash
[[ $1 == -k && $2 == "$KERNELVERSION" && $3 == -F && $4 == filename ]] || exit 1
if [[ " ${KERNEL_MODULES-thunderbolt} " == *" $5 "* ]]; then
printf '/usr/lib/modules/%s/kernel/%s.ko.zst\n' "$KERNELVERSION" "$5"
elif [[ " ${KERNEL_BUILTINS-} " == *" $5 "* ]]; then
printf '(builtin)\n'
else
exit 1
fi
SH
chmod +x "$test_tmp/modinfo"
for fixture in "$test_tmp"/platforms/*; do
cp "$test_tmp/modinfo" "$fixture/bin/"
done
# mkinitcpio's stock mkinitcpio.conf, which the baseline replaces, plus every
# drop-in Omarchy ships, on a machine with no PCI devices.
new_etc() {
rm -rf "$etc" "$devices"
mkdir -p "$etc/mkinitcpio.conf.d" "$devices"
cat >"$etc/mkinitcpio.conf" <<'CONF'
MODULES=()
BINARIES=()
FILES=()
HOOKS=(base systemd autodetect microcode modconf kms keyboard sd-vconsole block filesystems fsck)
CONF
cp "$ROOT"/etc/mkinitcpio.conf.d/*.conf "$etc/mkinitcpio.conf.d/"
}
# $1 names the drop-in; stdin is its content.
drop_in() {
cat >"$etc/mkinitcpio.conf.d/$1"
}
# Each argument is a PCI device as "vendor:class", in sysfs's own format.
pci_devices() {
local index=0 spec slot
for spec in "$@"; do
slot=$(printf '0000:%02x:00.0' "$index")
mkdir -p "$devices/$slot"
printf '%s\n' "${spec%%:*}" >"$devices/$slot/vendor"
printf '%s\n' "${spec##*:}" >"$devices/$slot/class"
index=$((index + 1))
done
}
# mkinitcpio appends the drop-ins to mkinitcpio.conf in its own order and
# sources the result once. Do the same on a platform fixture and print what the
# image is built from. The vconsole.conf entry comes from the host's own file,
# so it is left out of FILES.
compose() {
local fixture="$test_tmp/platforms/$1" config="$test_tmp/buildconfig" conf
local -a conf_files=()
mapfile -d '' conf_files < <(LC_ALL=C.UTF-8 find "$etc/mkinitcpio.conf.d" -maxdepth 1 -xtype f -name '*.conf' -print0 |
sed -z 's/.*\///' | LC_ALL=C.UTF-8 sort -zVu)
cat -- "$etc/mkinitcpio.conf" >"$config"
for conf in "${conf_files[@]}"; do
cat -- "$etc/mkinitcpio.conf.d/$conf" >>"$config"
done
local path="$fixture/bin:$ROOT/bin:$PATH"
[[ $1 != "no-detector" ]] || path="$fixture/bin"
KERNELVERSION=6.99.0-test OMARCHY_PROC_ROOT="$fixture/proc" OMARCHY_PCI_DEVICES_PATH="$devices" PATH="$path" "$BASH" -c '
. "$1" || exit 1
files=()
for file in "${FILES[@]}"; do
[[ $file == /etc/vconsole.conf ]] || files+=("$file")
done
printf "HOOKS=%s\nMODULES=%s\nFILES=%s\n" "${HOOKS[*]}" "${MODULES[*]}" "${files[*]}"
' -- "$config"
}
assert_composed() {
local description="$1" platform="$2" hooks="$3" modules="$4" files="$5"
local expected actual
expected=$(printf 'HOOKS=%s\nMODULES=%s\nFILES=%s\n' "$hooks" "$modules" "$files")
actual=$(compose "$platform") || fail "$description" "the drop-ins do not source cleanly"
[[ $actual == "$expected" ]] ||
fail "$description" "expected:"$'\n'"$expected"$'\n'"actual:"$'\n'"$actual"
pass "$description"
}
assert_hooks() {
local description="$1" platform="$2" expected="$3" actual
actual=$(compose "$platform") || fail "$description" "the drop-ins do not source cleanly"
actual=$(sed -n 's/^HOOKS=//p' <<<"$actual")
[[ $actual == "$expected" ]] ||
fail "$description" "expected: $expected"$'\n'"actual: $actual"
pass "$description"
}
# Each platform starts from its own baseline, whatever mkinitcpio.conf says.
new_etc
assert_hooks "Apple Silicon starts from the systemd baseline" aarch64-apple "$apple_hooks"
assert_hooks "plain aarch64 starts from the Omarchy baseline" aarch64 "$omarchy_hooks"
assert_hooks "x86 starts from the Omarchy baseline" x86 "$omarchy_hooks"
assert_hooks "without the detector the Omarchy baseline stays" no-detector "$omarchy_hooks"
if composed=$(compose contradiction 2>"$test_tmp/contradiction.err"); then
fail "a platform the detector cannot place stops the build" "composed: $composed"
fi
grep -q "contradictory platform identity" "$test_tmp/contradiction.err" ||
fail "a platform the detector cannot place stops the build" "stderr: $(<"$test_tmp/contradiction.err")"
pass "a platform the detector cannot place stops the build"
# A Mac that set up its own initramfs before the Apple boot package keeps it:
# busybox encrypt unlocks through cryptdevice=, which sd-encrypt cannot parse,
# and the asahi hook carries the firmware. The asahi hook marks such a root off
# a Mac too. Busybox encrypt alone means nothing off Apple Silicon.
legacy_encrypted="base udev plymouth autodetect microcode modconf kms keyboard keymap consolefont block encrypt asahi filesystems fsck"
legacy_plain="base udev autodetect microcode modconf kms keyboard keymap consolefont block asahi filesystems fsck"
for legacy in "$legacy_encrypted" "$legacy_plain" "${legacy_encrypted/ asahi / }"; do
new_etc
sed -i "s/^HOOKS=.*/HOOKS=($legacy)/" "$etc/mkinitcpio.conf"
assert_hooks "a Mac keeps its own HOOKS=($legacy)" aarch64-apple "$legacy"
done
new_etc
sed -i "s/^HOOKS=.*/HOOKS=($legacy_plain)/" "$etc/mkinitcpio.conf"
assert_hooks "an asahi root keeps its HOOKS off a Mac" aarch64 "$legacy_plain"
# A legacy GRUB Mac has the asahi hook right after base, where Asahi's images
# put it, and may run a runtime older than the detector.
legacy_grub="base asahi udev plymouth autodetect microcode modconf kms keyboard keymap consolefont block encrypt filesystems fsck"
for platform in aarch64-apple no-detector; do
for legacy in "$legacy_grub" "${legacy_grub/ encrypt / }"; do
new_etc
sed -i "s/^HOOKS=.*/HOOKS=($legacy)/" "$etc/mkinitcpio.conf"
assert_hooks "a legacy GRUB Mac keeps HOOKS=($legacy) ($platform)" "$platform" "$legacy"
done
done
new_etc
sed -i "s/^HOOKS=.*/HOOKS=(base udev autodetect modconf block encrypt filesystems fsck)/" "$etc/mkinitcpio.conf"
assert_hooks "busybox encrypt off Apple Silicon still gets the Omarchy baseline" x86 "$omarchy_hooks"
new_etc
sed -i "s/^HOOKS=.*/HOOKS=(base udev autodetect modconf kms keyboard keymap consolefont block filesystems fsck)/" "$etc/mkinitcpio.conf"
assert_hooks "a Mac with a stock busybox line and no asahi hook gets the systemd baseline" aarch64-apple "$apple_hooks"
# A platform fragment sorts after the baseline and before omarchy_hooks.conf,
# as omarchy-mac-boot's 90- drop-ins do. Its hooks must reach the image.
new_etc
drop_in 90-platform-fragment.conf <<'CONF'
_fragment_hooks=()
for _fragment_hook in "${HOOKS[@]}"; do
[[ $_fragment_hook == "filesystems" ]] && _fragment_hooks+=(platform-firmware)
_fragment_hooks+=("$_fragment_hook")
done
HOOKS=("${_fragment_hooks[@]}" platform-late)
unset _fragment_hooks _fragment_hook
CONF
assert_hooks "a platform fragment's hooks survive on Apple Silicon" aarch64-apple \
"base systemd plymouth autodetect microcode modconf kms keyboard sd-vconsole block platform-firmware filesystems fsck platform-late"
assert_hooks "a platform fragment's hooks survive on plain aarch64" aarch64 \
"base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt platform-firmware filesystems fsck btrfs-overlayfs platform-late"
assert_hooks "a platform fragment's hooks survive on x86" x86 \
"base udev plymouth keyboard autodetect microcode modconf kms keymap consolefont block encrypt platform-firmware filesystems fsck btrfs-overlayfs platform-late"
# The NVIDIA filter still runs after the fragment and removes only kms.
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
pci_devices 0x10de:0x030000
assert_hooks "the NVIDIA filter keeps a platform fragment's hooks" x86 \
"base udev plymouth keyboard autodetect microcode modconf keymap consolefont block encrypt platform-firmware filesystems fsck btrfs-overlayfs platform-late"
# Existing x86 configurations build the image they built before the baseline
# moved: the same HOOKS, MODULES and FILES.
new_etc
assert_composed "a machine without hardware drop-ins is unchanged" x86 \
"$omarchy_hooks" "thunderbolt" ""
new_etc
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
pci_devices 0x10de:0x030000
assert_composed "NVIDIA-only drops only kms" x86 \
"$omarchy_hooks_without_kms" "$nvidia_modules thunderbolt" ""
new_etc
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
pci_devices 0x8086:0x030000 0x10de:0x030200
assert_composed "hybrid graphics keeps kms for the iGPU" x86 \
"$omarchy_hooks" "$nvidia_modules thunderbolt" ""
new_etc
pci_devices 0x10de:0x030000
assert_composed "NVIDIA-only without early nvidia_drm keeps kms" x86 \
"$omarchy_hooks" "thunderbolt" ""
new_etc
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
drop_in omarchy_resume.conf <<<"HOOKS+=(resume)"
drop_in 99-omarchy-provisioning-key.conf <<<"FILES+=(/etc/omarchy/provisioning.key)"
pci_devices 0x10de:0x030000
assert_composed "NVIDIA-only with hibernation and a provisioning key is unchanged" x86 \
"$omarchy_hooks_without_kms resume" "$nvidia_modules thunderbolt" "/etc/omarchy/provisioning.key"
new_etc
drop_in apple-t2.conf <<<"MODULES+=(t2bce_vhci usbhid hid_apple hid_generic xhci_pci xhci_hcd)"
assert_composed "a T2 Mac is unchanged" x86 \
"$omarchy_hooks" "t2bce_vhci usbhid hid_apple hid_generic xhci_pci xhci_hcd thunderbolt" ""
new_etc
drop_in macbook_spi_modules.conf <<<"MODULES=(applespi intel_lpss_pci spi_pxa2xx_platform)"
assert_composed "an SPI keyboard MacBook is unchanged" x86 \
"$omarchy_hooks" "applespi intel_lpss_pci spi_pxa2xx_platform thunderbolt" ""
new_etc
drop_in nvidia.conf <<<"MODULES+=($nvidia_modules)"
drop_in surface_device_modules.conf <<<"MODULES=(pinctrl_tigerlake surface_aggregator surface_aggregator_registry surface_aggregator_hub surface_hid_core surface_hid surface_kbd intel_lpss_pci 8250_dw)"
pci_devices 0x8086:0x030000
assert_composed "a Surface is unchanged" x86 \
"$omarchy_hooks" "pinctrl_tigerlake surface_aggregator surface_aggregator_registry surface_aggregator_hub surface_hid_core surface_hid surface_kbd intel_lpss_pci 8250_dw thunderbolt" ""
# Snapdragon and other aarch64 machines build the same image as x86, whatever
# systemd line their mkinitcpio.conf starts from.
for platform in aarch64; do
new_etc
drop_in omarchy_resume.conf <<<"HOOKS+=(resume)"
drop_in 99-omarchy-provisioning-key.conf <<<"FILES+=(/etc/omarchy/provisioning.key)"
assert_composed "$platform with hibernation and a provisioning key is unchanged" "$platform" \
"$omarchy_hooks resume" "thunderbolt" "/etc/omarchy/provisioning.key"
done
# thunderbolt is early-loaded only where the kernel builds it as a module. A
# systemd initramfs writes MODULES into modules-load.d as they are, so neither
# an optional thunderbolt? nor a module the kernel lacks may reach it.
new_etc
assert_composed "Apple Silicon early-loads the Aurora kernel's thunderbolt module" aarch64-apple \
"$apple_hooks" "thunderbolt" ""
new_etc
KERNEL_MODULES="" assert_composed "a plain aarch64 kernel without thunderbolt leaves it out" aarch64 \
"$omarchy_hooks" "" ""
new_etc
KERNEL_MODULES="" KERNEL_BUILTINS=thunderbolt assert_composed "a kernel with thunderbolt built in leaves it out" x86 \
"$omarchy_hooks" "" ""
+2
View File
@@ -97,6 +97,8 @@ cat >"$fake_bin/omarchy-hyprland-monitor-clamshell" <<'SH'
exit 0
SH
cp "$fake_bin/omarchy-hyprland-monitor-clamshell" "$fake_bin/omarchy-system-lid-inhibit"
chmod +x "$fake_bin"/*
ln -s "$ROOT/bin/omarchy-hyprland-monitor-modeless" "$fake_bin/omarchy-hyprland-monitor-modeless"
+6 -1
View File
@@ -60,7 +60,8 @@ grep -F 'hyprctl monitors all -j' "$ROOT/bin/omarchy-hyprland-monitor-modeless"
pass "modeless helper sees mirrors and ignores monitors disabled on purpose"
grep -F 'omarchy-hw-laptop-closed && omarchy-hw-external-monitors' "$hw_clamshell" >/dev/null
grep -F '/proc/acpi/button/lid/*/state' "$hw_laptop_closed" >/dev/null
grep -F 'org.freedesktop.login1.Manager LidClosed' "$hw_laptop_closed" >/dev/null
grep -F 'OMARCHY_ACPI_LID_PATH:-/proc/acpi/button/lid' "$hw_laptop_closed" >/dev/null
pass "clamshell helper detects closed-lid external monitor state"
# A mirrored external is absent from plain `monitors`, so asking without `all`
@@ -99,6 +100,10 @@ pass "internal mirror helper recovers when no active external display remains"
grep -F 'switch:on:Lid Switch", nil, "omarchy-system-lid-close"' "$utilities" >/dev/null
grep -F 'switch:off:Lid Switch", nil, "omarchy-hyprland-monitor-clamshell"' "$utilities" >/dev/null
grep -F 'switch:on:Apple SMC power/lid events", nil, "omarchy-system-lid-close"' "$utilities" >/dev/null ||
fail "closing an Apple Silicon lid runs the same lid-close as any laptop"
grep -F 'switch:off:Apple SMC power/lid events", nil, "omarchy-hyprland-monitor-clamshell"' "$utilities" >/dev/null ||
fail "opening an Apple Silicon lid restores the panel like any laptop"
pass "lid switch bindings lock on close and reconcile clamshell display state"
grep -F 'omarchy-hyprland-monitor-clamshell >/dev/null 2>&1 || true' "$system_wake" >/dev/null
+17
View File
@@ -259,6 +259,18 @@ assertDeepEqual(
'notifications ignore a left bar for popup placement'
)
// A toast clears the bar as thick as it is on the toast's own screen: a notch
// floor can make a top bar thicker on one screen only. A bar without
// per-screen sizes (a plugin bar) counts its configured size, and a hidden or
// missing bar the default size, as before.
const notchBar = { barHidden: false, barSize: 26, barSizeFor: name => (name === 'eDP-1' ? 40 : 26) }
assertEqual(notifications.barClearance(notchBar, 'eDP-1', 26, 6), 46, 'a toast clears a notch-floored bar on its screen')
assertEqual(notifications.barClearance(notchBar, 'DP-1', 26, 6), 32, 'a toast on another screen clears the configured size')
assertEqual(notifications.barClearance({ barHidden: false, barSize: 30 }, 'eDP-1', 26, 6), 36, 'a bar without per-screen sizes counts its configured size')
assertEqual(notifications.barClearance({ barHidden: true, barSize: 30, barSizeFor: () => 40 }, 'eDP-1', 26, 6), 32, 'a hidden bar counts the default size')
assertEqual(notifications.barClearance(null, 'eDP-1', 28, 6), 34, 'a missing bar counts the default size')
assertEqual(notifications.barClearance({ barHidden: false, barSize: undefined }, 'eDP-1', 26, 6), 6, 'an unreadable size counts as none')
const notification = {
id: 12,
appName: 'Mail',
@@ -599,6 +611,11 @@ assert(!('exec' in legacyRestored), 'a restored legacy popup drops the old exec
assertEqual(notifications.parseExecArgv(legacyRestored.execArgv || ''), null, 'a restored legacy popup has no runnable click action')
const serviceQml = fs.readFileSync(path.join(root, 'shell/plugins/notifications/Service.qml'), 'utf8')
assert(
/function barClearanceFor\(screenName\) \{\s*return NotificationLogic\.barClearance\(shell \? shell\.bar : null, screenName, defaultBarSize, Style\.gapsOut\)\s*\}/.test(serviceQml) &&
/NotificationLogic\.popupPlacement\(\s*service\.barPosition, service\.barClearanceFor\(modelData \? modelData\.name : ""\), Style\.gapsOut\)/.test(serviceQml),
'each screen\'s toasts clear the bar as thick as it is on that screen'
)
assert(
/readonly property int historyLimit: 10/.test(serviceQml),
'notifications service keeps the last ten notifications in history'
+32 -3
View File
@@ -7,7 +7,9 @@ source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
tmp_dir=$(mktemp -d)
trap 'rm -rf "$tmp_dir"' EXIT
baseline_conf="$ROOT/etc/mkinitcpio.conf.d/00-omarchy-hooks.conf"
hooks_conf="$ROOT/etc/mkinitcpio.conf.d/omarchy_hooks.conf"
fake_platform "$tmp_dir/generic" x86
# Each argument is a PCI device as "vendor:class", in sysfs's own format.
write_pci_devices() {
@@ -26,8 +28,8 @@ write_pci_devices() {
done
}
# Sources the hook config the way mkinitcpio does — with earlier drop-ins
# already applied — and prints the resulting HOOKS. mkinitcpio does not run
# Sources the hook config the way mkinitcpio does — after the baseline and
# the earlier drop-ins — and prints the resulting HOOKS. mkinitcpio does not run
# under set -u, but the config must survive it, so source under it anyway.
# "unset" leaves MODULES undeclared entirely.
resolved_hooks() {
@@ -37,9 +39,11 @@ resolved_hooks() {
# The vconsole block sources the host's /etc/vconsole.conf, which may set
# only KEYMAP; predefine XKBLAYOUT so its expansion survives set -u and the
# test stays independent of the machine it runs on.
OMARCHY_PCI_DEVICES_PATH="$tmp_dir/devices" bash -uc "
OMARCHY_PCI_DEVICES_PATH="$tmp_dir/devices" OMARCHY_PROC_ROOT="$tmp_dir/generic/proc" \
PATH="$tmp_dir/generic/bin:$ROOT/bin:$PATH" bash -uc "
FILES=()
XKBLAYOUT=us
source '$baseline_conf'
$modules_decl
source '$hooks_conf'
echo \"\${HOOKS[*]}\"
@@ -101,3 +105,28 @@ write_pci_devices 0x10de:0x030000
mkdir -p "$tmp_dir/devices/0000:01:00.0"
assert_hooks "unreadable device beside an NVIDIA GPU keeps kms" \
"$nvidia_modules" "$with_kms"
mkdir -p "$tmp_dir/conf" "$tmp_dir/bin"
cp "$baseline_conf" "$tmp_dir/conf/00-omarchy-hooks.conf"
cp "$hooks_conf" "$tmp_dir/conf/omarchy_hooks.conf"
printf 'MODULES=(%s)\n' "$nvidia_modules" >"$tmp_dir/conf/nvidia.conf"
printf '#!/bin/bash\nexec "$@"\n' >"$tmp_dir/bin/sudo"
printf '#!/bin/bash\necho rebuild >>"$TEST_REBUILD_LOG"\n' >"$tmp_dir/bin/limine-mkinitcpio"
chmod +x "$tmp_dir/bin/"*
export PATH="$tmp_dir/bin:$tmp_dir/generic/bin:$ROOT/bin:$PATH"
export OMARCHY_PROC_ROOT="$tmp_dir/generic/proc" OMARCHY_PCI_DEVICES_PATH="$tmp_dir/devices"
export OMARCHY_MKINITCPIO_HOOKS_CONF="$tmp_dir/conf/omarchy_hooks.conf"
export OMARCHY_MKINITCPIO_NVIDIA_CONF="$tmp_dir/conf/nvidia.conf"
export OMARCHY_KMS_REBUILD_MARKER="$tmp_dir/rebuilt" TEST_REBUILD_LOG="$tmp_dir/rebuild-log"
write_pci_devices 0x1002:0x030000 0x10de:0x030200
bash -euo pipefail "$ROOT/migrations/1786605598.sh" >/dev/null
[[ ! -e $TEST_REBUILD_LOG && ! -e $OMARCHY_KMS_REBUILD_MARKER ]] || fail "the NVIDIA migration skips hybrid graphics with the split hook baseline"
pass "the NVIDIA migration reads the new baseline and skips hybrid graphics"
write_pci_devices 0x10de:0x030000
bash -euo pipefail "$ROOT/migrations/1786605598.sh" >/dev/null
[[ $(<"$TEST_REBUILD_LOG") == "rebuild" && -e $OMARCHY_KMS_REBUILD_MARKER ]] || fail "the NVIDIA migration still rebuilds NVIDIA-only graphics"
bash -euo pipefail "$ROOT/migrations/1786605598.sh" >/dev/null
[[ $(<"$TEST_REBUILD_LOG") == "rebuild" ]] || fail "the NVIDIA migration respects its completed marker"
pass "the NVIDIA migration still rebuilds NVIDIA-only graphics once"
+173
View File
@@ -0,0 +1,173 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command python3
require_command pacman-conf
# Every platform's repositories are a pacman.conf and mirrorlist per channel,
# copied into place whole on a channel change and at install finalization, as
# x86_64's always were.
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
export OMARCHY_PATH="$ROOT"
source "$ROOT/install/helpers/pacman.sh"
platforms="x86 aarch64 aarch64-apple"
# aarch64 has edge alone: stable and rc there would install the release line,
# which has no aarch64 support.
channels_for() {
if [[ $1 == "x86" ]]; then echo "stable rc edge"; else echo "edge"; fi
}
# ── the templates ────────────────────────────────────────────────────────────
[[ $(omarchy_pacman_templates x86) == "$ROOT/default/pacman" ]] || fail "x86 keeps its templates where they were"
[[ $(omarchy_pacman_templates aarch64) == "$ROOT/default/pacman/aarch64" ]] || fail "plain aarch64 uses the aarch64 templates"
[[ $(omarchy_pacman_templates aarch64-apple) == "$ROOT/default/pacman/aarch64-apple" ]] || fail "Apple Silicon uses its own templates"
! omarchy_pacman_templates riscv 2>/dev/null || fail "an unknown platform has no templates"
pass "each platform's templates sit in a directory of their own, x86_64's where they always were"
[[ $(omarchy_pacman_default_channel x86) == stable ]] || fail "x86 defaults to stable"
for platform in aarch64 aarch64-apple; do
[[ $(omarchy_pacman_default_channel "$platform") == edge ]] || fail "$platform defaults to edge"
done
! omarchy_pacman_default_channel riscv 2>/dev/null || fail "an unknown platform has no default channel"
pass "x86 defaults to stable and every aarch64 platform to edge"
# pacman reads each repository list from the template, with its mirrorlist
# standing in for /etc/pacman.d/mirrorlist.
repos() {
local templates=$1 channel=$2
sed "s|/etc/pacman.d/mirrorlist|$templates/mirrorlist-$channel|" "$templates/pacman-$channel.conf" >"$work/pacman.conf"
pacman-conf --config "$work/pacman.conf" --repo-list | tr '\n' ' '
}
for platform in $platforms; do
templates=$(omarchy_pacman_templates "$platform")
for channel in stable rc; do
[[ $platform == "x86" ]] && continue
[[ ! -e $templates/pacman-$channel.conf && ! -e $templates/mirrorlist-$channel ]] ||
fail "$platform has no $channel template or mirrorlist"
done
for channel in $(channels_for "$platform"); do
[[ -f $templates/pacman-$channel.conf && -f $templates/mirrorlist-$channel ]] ||
fail "$platform has a $channel template and mirrorlist"
list=$(repos "$templates" "$channel") || fail "$platform $channel: pacman reads the template"
case $platform in
x86) expected="omarchy core extra multilib " ;;
aarch64) expected="omarchy core extra alarm aur " ;;
aarch64-apple) expected="omarchy asahi-alarm core extra alarm aur " ;;
esac
[[ $list == "$expected" ]] || fail "$platform $channel: repositories in order" "$list"
# $arch stays literal: pacman fills it in on the machine.
server=$(sed -n '/^\[omarchy\]/,/^\[/s/^Server = //p' "$templates/pacman-$channel.conf")
[[ $server == "https://pkgs.omarchy.org/$channel/\$arch" ]] ||
fail "$platform $channel: Omarchy's $channel repository" "$server"
done
done
pass "x86 has templates for stable, rc and edge and aarch64 for edge alone, each with its repositories in order"
# ── install finalization ─────────────────────────────────────────────────────
finalize_bin=$work/finalize-bin
mkdir -p "$finalize_bin"
printf '#!/bin/bash\necho "$PLATFORM"\n' >"$finalize_bin/omarchy-hw-platform"
for command in omarchy-pkg-add pacman-key; do
printf '#!/bin/bash\nexit 0\n' >"$finalize_bin/$command"
done
chmod +x "$finalize_bin"/*
sed "s|/etc/pacman|$work/etc/pacman|g" "$ROOT/install/post-install/pacman.sh" >"$work/finalize.sh"
mkdir -p "$work/install/hardware"
: >"$work/install/hardware/pacman.sh"
# Finalization asks the image helper whether this is an image build, which root
# answers from the real /var/lib/omarchy; everyone else from the test's root.
if (( EUID == 0 )); then
skip "install finalization copies the platform's channel template and mirrorlist (refuses to run as root)"
else
for platform in $platforms; do
templates=$(omarchy_pacman_templates "$platform")
# An install built for a channel the platform has no templates for, or for
# none, gets the platform's default channel.
for channel in stable rc edge ""; do
expected=$channel
[[ -n $expected && -f $templates/pacman-$expected.conf ]] || expected=$(omarchy_pacman_default_channel "$platform")
rm -rf "$work/etc"
mkdir -p "$work/etc/pacman.d"
printf 'offline\n' | tee "$work/etc/pacman.conf" >"$work/etc/pacman.d/mirrorlist"
OMARCHY_IMAGE_ROOT=$work OMARCHY_MIRROR=$channel PLATFORM=$platform OMARCHY_INSTALL="$work/install" PATH="$finalize_bin:$PATH" \
bash -e -c 'source "$1"' bash "$work/finalize.sh" >/dev/null || fail "$platform finalization on '$channel'"
cmp -s "$work/etc/pacman.conf" "$templates/pacman-$expected.conf" || fail "$platform '$channel': finalization copies the $expected template"
cmp -s "$work/etc/pacman.d/mirrorlist" "$templates/mirrorlist-$expected" || fail "$platform '$channel': finalization copies the $expected mirrorlist"
done
done
pass "install finalization copies the platform's channel template and mirrorlist, or its default channel's when it has none for that channel"
fi
# ── refresh through the command, with every privileged step a stand-in ───────
rm -rf "$work"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
copy_boundary_file bin/omarchy-refresh-pacman
refresh() {
"$SUDO_TEST_ROOT/bin/omarchy-refresh-pacman" "$@" >"$boundary_tmp/output" 2>&1
}
events() {
grep -vE '^sudo (-h|-k)$' "$SUDO_TEST_LOG" || true
}
for platform in $platforms; do
case $platform in
x86) templates=$SUDO_TEST_ROOT/default/pacman ;;
aarch64) templates=$SUDO_TEST_ROOT/default/pacman/aarch64 ;;
aarch64-apple) templates=$SUDO_TEST_ROOT/default/pacman/aarch64-apple ;;
esac
# No channel named refreshes to the platform's default one.
for channel in $(channels_for "$platform") ""; do
reset_boundary
SUDO_TEST_PLATFORM=$platform refresh $channel || fail "$platform refreshes to '$channel'" "$(cat "$boundary_tmp/output")"
[[ -n $channel ]] || channel=$(omarchy_pacman_default_channel "$platform")
python3 - "$SUDO_TEST_LOG" "$templates" "$channel" <<'PY'
import sys
events = [e for e in open(sys.argv[1]).read().splitlines() if e not in ('sudo -h', 'sudo -k')]
templates, channel = sys.argv[2:]
expected = [
'step:cp -f /etc/pacman.conf /etc/pacman.conf.bak',
'step:cp -f /etc/pacman.d/mirrorlist /etc/pacman.d/mirrorlist.bak',
f'step:cp -f {templates}/pacman-{channel}.conf /etc/pacman.conf',
f'step:cp -f {templates}/mirrorlist-{channel} /etc/pacman.d/mirrorlist',
]
copies = [e for e in events if e.startswith('step:cp ')]
assert copies == expected, events
hook = events.index('step:omarchy-hook pre-refresh-pacman')
transaction = events.index('step:pacman -Syyuu --noconfirm')
assert events.index(expected[-1]) < hook < transaction, events
PY
assert_boundary_cold "$platform $channel"
done
done
pass "a refresh backs up and copies the platform's channel template and mirrorlist, or its default channel's when none is named, then runs the cold hook and the upgrade"
# A channel the platform has no template for (stable or rc on aarch64), or a
# machine whose platform can't be told, stops before anything changes.
for platform in aarch64 aarch64-apple; do
for channel in stable rc; do
reset_boundary
if SUDO_TEST_PLATFORM=$platform refresh "$channel"; then fail "$platform: $channel is refused"; fi
[[ -z $(events) ]] || fail "$platform: $channel stops before anything" "$(events)"
grep -q "Omarchy has no $channel channel for $platform" "$boundary_tmp/output" || fail "the refusal says why" "$(cat "$boundary_tmp/output")"
assert_boundary_cold "$platform $channel"
done
done
cat >"$SUDO_TEST_ROOT/bin/omarchy-hw-platform" <<'STUB'
#!/bin/bash
exit 1
STUB
reset_boundary
if refresh stable; then fail "an unknown platform is refused"; fi
[[ -z $(events) ]] || fail "an unknown platform stops before anything" "$(events)"
assert_boundary_cold "unknown platform"
pass "a channel without a template, or a machine whose platform can't be told, changes nothing"
+136
View File
@@ -0,0 +1,136 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# Browser and Steam installs, and Steam's removal, call the platform's own
# hooks through the real dispatcher: a Mac fixture with omarchy-mac's
# entrypoints, and x86.
require_platform_fixtures "app-install hooks on platform fixtures"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
fake_platform "$tmp/apple" aarch64-apple
fake_platform "$tmp/x86" x86
# The hooks record what they were called with and which flags files the
# browser install had written by then.
lifecycle=$tmp/lifecycle
mkdir -p "$lifecycle/usr/lib/omarchy/mac"
for operation in post-install pre-remove; do
cat >"$lifecycle/usr/lib/omarchy/mac/$operation" <<SH
#!/bin/bash
printf '%s %s\n' "$operation" "\$*" >>"$tmp/calls"
ls "\$HOME/.config" 2>/dev/null | grep -- '-flags.conf$' >"$tmp/flags-at-hook" || true
[[ ! -e $tmp/fail ]]
SH
chmod 755 "$lifecycle/usr/lib/omarchy/mac/$operation"
done
chmod -R go-w "$lifecycle"
stub_bin=$tmp/bin
omarchy_path=$tmp/omarchy
mkdir -p "$stub_bin" "$omarchy_path/install/helpers" "$omarchy_path/config"
printf 'browser_policy_setup_dir() { :; }\nbrowser_policy_setup_firefox_distribution() { :; }\nas_root() { "$@"; }\n' \
>"$omarchy_path/install/helpers/browser-policy.sh"
cp "$ROOT/config/chromium-flags.conf" "$omarchy_path/config/"
for command in omarchy-pkg-add omarchy-pkg-aur-add omarchy-pkg-drop omarchy-install-gaming-gpu-lib32; do
printf '#!/bin/bash\nprintf "%%s %%s\\n" "%s" "$*" >>"%s"\n' "$command" "$tmp/calls" >"$stub_bin/$command"
done
for command in omarchy-install-chromium-copy-url omarchy-install-chromium-ytdlp omarchy-theme-set-browser setsid; do
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/$command"
done
chmod +x "$stub_bin"/*
run() {
local platform=$1
shift
rm -f "$tmp/calls" "$tmp/flags-at-hook"
rm -rf "$tmp/home"
mkdir -p "$tmp/home"
HOME="$tmp/home" OMARCHY_PATH="$omarchy_path" OMARCHY_OPT_PATH="$tmp/opt" \
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$lifecycle" \
PATH="$stub_bin:$tmp/$platform/bin:$ROOT/bin:$PATH" bash "$@" >"$tmp/output" 2>&1
}
# ── browsers ─────────────────────────────────────────────────────────────────
declare -A flags=(
[chromium]=chromium-flags.conf [chrome]=chrome-flags.conf [edge]=microsoft-edge-stable-flags.conf
[brave]=brave-flags.conf [brave-origin]=brave-origin-flags.conf [firefox]="" [zen]=""
)
for browser in chromium chrome edge brave brave-origin firefox zen; do
run apple "$ROOT/bin/omarchy-install-browser" "$browser" || fail "apple: $browser installs" "$(cat "$tmp/output")"
[[ $(grep -c '^post-install ' "$tmp/calls") == 1 ]] && grep -qx "post-install $browser" "$tmp/calls" ||
fail "apple: a $browser install runs post-install $browser once" "$(cat "$tmp/calls")"
[[ $(cat "$tmp/flags-at-hook") == "${flags[$browser]}" ]] ||
fail "apple: the $browser hook runs once its flags file is written" "$(cat "$tmp/flags-at-hook")"
[[ $(grep -n . "$tmp/calls" | grep -m1 'post-install' | cut -d: -f1) -gt 1 ]] ||
fail "apple: the $browser hook runs after its package step" "$(cat "$tmp/calls")"
done
pass "apple: every browser install runs post-install <browser> after its package and flags"
touch "$tmp/fail"
for browser in chromium brave; do
if run apple "$ROOT/bin/omarchy-install-browser" "$browser"; then
fail "apple: a failed $browser hook fails the install"
fi
! grep -q 'browser installed' "$tmp/output" || fail "apple: a failed $browser hook is not announced as installed" "$(cat "$tmp/output")"
done
pass "apple: a failed browser hook fails the install"
for browser in chromium brave-origin zen; do
run x86 "$ROOT/bin/omarchy-install-browser" "$browser" || fail "x86: $browser installs" "$(cat "$tmp/output")"
! grep -q '^post-install' "$tmp/calls" || fail "x86: a $browser install runs no hook" "$(cat "$tmp/calls")"
done
rm -f "$tmp/fail"
pass "x86: browser installs run no hook, even with Mac entrypoints on disk"
# ── Steam ────────────────────────────────────────────────────────────────────
run apple "$ROOT/bin/omarchy-install-gaming-steam" || fail "apple: Steam installs" "$(cat "$tmp/output")"
[[ $(cat "$tmp/calls") == $'omarchy-install-gaming-gpu-lib32 \nomarchy-pkg-add steam\npost-install steam' ]] ||
fail "apple: Steam's hook runs after the 32-bit drivers and the package" "$(cat "$tmp/calls")"
run apple "$ROOT/bin/omarchy-remove-gaming-steam" || fail "apple: Steam is removed" "$(cat "$tmp/output")"
[[ $(head -n 2 "$tmp/calls") == $'pre-remove steam\nomarchy-pkg-drop steam' ]] ||
fail "apple: Steam's pre-remove hook runs before the package goes" "$(cat "$tmp/calls")"
touch "$tmp/fail"
if run apple "$ROOT/bin/omarchy-install-gaming-steam"; then
fail "apple: a failed Steam hook fails the install"
fi
if run apple "$ROOT/bin/omarchy-remove-gaming-steam"; then
fail "apple: a failed pre-remove hook stops the removal"
fi
! grep -q '^omarchy-pkg-drop' "$tmp/calls" || fail "apple: a failed pre-remove hook removes nothing" "$(cat "$tmp/calls")"
pass "apple: Steam's install and removal run their hooks in order, and fail with them"
run x86 "$ROOT/bin/omarchy-install-gaming-steam" || fail "x86: Steam installs" "$(cat "$tmp/output")"
[[ $(cat "$tmp/calls") == $'omarchy-install-gaming-gpu-lib32 \nomarchy-pkg-add steam' ]] ||
fail "x86: Steam installs as before" "$(cat "$tmp/calls")"
run x86 "$ROOT/bin/omarchy-remove-gaming-steam" || fail "x86: Steam is removed" "$(cat "$tmp/output")"
[[ $(cat "$tmp/calls") == "omarchy-pkg-drop steam" ]] || fail "x86: Steam is removed as before" "$(cat "$tmp/calls")"
rm -f "$tmp/fail"
pass "x86: Steam installs and is removed as before, running no hook"
# ── 32-bit drivers ───────────────────────────────────────────────────────────
# Without an Intel, AMD or NVIDIA GPU there is nothing to add, which must not
# fail the Steam install that asked.
gpu_bin=$tmp/gpu-bin
mkdir -p "$gpu_bin"
printf '#!/bin/bash\necho "00:02.0 VGA compatible controller: %s"\n' '${GPU:-Apple Inc. AGX}' >"$gpu_bin/lspci"
printf '#!/bin/bash\nexit 1\n' >"$gpu_bin/omarchy-hw-nvidia-gsp"
printf '#!/bin/bash\nexit 1\n' >"$gpu_bin/omarchy-hw-nvidia-without-gsp"
printf '#!/bin/bash\necho "omarchy-pkg-add $*" >>"%s"\n' "$tmp/calls" >"$gpu_bin/omarchy-pkg-add"
chmod +x "$gpu_bin"/*
rm -f "$tmp/calls"
PATH="$gpu_bin:$PATH" bash "$ROOT/bin/omarchy-install-gaming-gpu-lib32" >"$tmp/output" 2>&1 ||
fail "no supported GPU is not a failure" "$(cat "$tmp/output")"
[[ ! -e $tmp/calls ]] || fail "no supported GPU installs nothing" "$(cat "$tmp/calls")"
GPU="Advanced Micro Devices, Inc. [AMD/ATI] Navi 31" PATH="$gpu_bin:$PATH" bash "$ROOT/bin/omarchy-install-gaming-gpu-lib32" >/dev/null 2>&1 ||
fail "an AMD GPU installs its 32-bit driver"
[[ $(cat "$tmp/calls") == "omarchy-pkg-add lib32-vulkan-radeon" ]] || fail "an AMD GPU gets lib32-vulkan-radeon" "$(cat "$tmp/calls")"
pass "32-bit drivers: a machine with no supported GPU adds nothing without failing"
+160
View File
@@ -0,0 +1,160 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_platform_fixtures "platform package lists"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
export OMARCHY_PATH="$ROOT"
export PATH="$ROOT/bin:$PATH"
names() {
sed -e 's/[[:space:]]*#.*$//' -e '/^[[:space:]]*$/d' "$@"
}
base=$(names "$ROOT/install/omarchy-base.packages")
aarch64=$(names "$ROOT/install/omarchy-aarch64.packages")
for platform in aarch64-apple aarch64 x86; do
fake_platform "$work/$platform" "$platform"
defaults=$(OMARCHY_PROC_ROOT="$work/$platform/proc" PATH="$work/$platform/bin:$ROOT/bin:$PATH" omarchy-pkg-defaults)
[[ $defaults == "$(omarchy-pkg-defaults "$platform")" ]] ||
fail "$platform: the detected platform selects its own list"
printf '%s\n' "$defaults" >"$work/$platform.packages"
[[ -z $(sort "$work/$platform.packages" | uniq -d) ]] || fail "$platform: each package is listed once"
# aarch64 platforms leave out the base packages only x86_64 builds.
expected_base=$base
[[ $platform == "x86" ]] || expected_base=$(grep -vxF -f <(names "$ROOT/install/omarchy-x86_64-only.packages") <<<"$base")
[[ $(head -n "$(wc -l <<<"$expected_base")" "$work/$platform.packages") == "$expected_base" ]] ||
fail "$platform: the base list comes first, unchanged"
case $platform in
x86)
[[ $defaults == "$base" ]] || fail "x86_64 installs exactly the base list"
[[ $defaults == "$(grep -v '^#' "$ROOT/install/omarchy-base.packages" | grep -v '^$')" ]] ||
fail "x86_64 installs what omarchy-reinstall-pkgs read from the base list before"
;;
*)
while IFS= read -r package; do
grep -Fxq "$package" "$work/$platform.packages" || fail "$platform: aarch64 addition $package"
done <<<"$aarch64"
;;
esac
# The Mac's packages, and wf-recorder, which records its screen: nothing else
# captures on Apple Silicon.
for package in omarchy-mac omarchy-mac-boot wf-recorder; do
if [[ $platform == "aarch64-apple" ]]; then
grep -Fxq "$package" "$work/$platform.packages" || fail "Apple Silicon adds $package"
else
! grep -Fxq "$package" "$work/$platform.packages" || fail "$platform: no $package"
fi
done
done
pass "each platform composes the base, architecture and platform lists"
# A hardware family's own list joins only that family's set, after the base and
# architecture lists; plain aarch64 composes the others alone.
tree="$work/tree"
mkdir -p "$tree/install"
cp "$ROOT"/install/omarchy-*.packages "$tree/install/"
printf '# test addition\nexample-board-support\nzram-generator\n' >"$tree/install/omarchy-aarch64-apple.packages"
for platform in aarch64-apple aarch64 x86; do
defaults=$(OMARCHY_PATH="$tree" omarchy-pkg-defaults "$platform")
if [[ $platform == "aarch64-apple" ]]; then
[[ $(tail -n 1 <<<"$defaults") == "example-board-support" ]] || fail "a platform list is added after the others" "$defaults"
(( $(grep -cx zram-generator <<<"$defaults") == 1 )) || fail "a name in two lists is installed once" "$defaults"
else
! grep -Fxq example-board-support <<<"$defaults" || fail "$platform: another platform's list stays out"
fi
done
pass "a platform list joins only its own platform's set"
# Image builders written before the platform names settled pass the old names.
for legacy in apple-silicon:aarch64-apple generic-aarch64:aarch64 generic:x86; do
[[ $(omarchy-pkg-defaults "${legacy%%:*}") == "$(omarchy-pkg-defaults "${legacy#*:}")" ]] ||
fail "the old name ${legacy%%:*} composes the ${legacy#*:} set"
done
pass "the old platform names compose the same sets"
# Base packages that only x86_64 builds stay out of every aarch64 set.
for platform in aarch64-apple aarch64 x86; do
defaults=$(OMARCHY_PATH="$ROOT" omarchy-pkg-defaults "$platform")
while read -r name; do
[[ -n $name && $name != \#* ]] || continue
if [[ $platform == "x86" ]]; then
grep -Fxq "$name" <<<"$defaults" || fail "x86_64 keeps $name"
else
! grep -Fxq "$name" <<<"$defaults" || fail "$platform leaves out the x86_64-only $name"
fi
done <"$ROOT/install/omarchy-x86_64-only.packages"
done
grep -Fxq superwhisper-bin "$ROOT/install/omarchy-x86_64-only.packages" || fail "superwhisper-bin is listed as x86_64-only"
pass "base packages only x86_64 builds stay out of every aarch64 set"
! omarchy-pkg-defaults riscv 2>/dev/null || fail "an unknown platform is refused"
failing_bin="$work/failing-bin"
mkdir -p "$failing_bin"
printf '#!/bin/bash\nexit 1\n' >"$failing_bin/omarchy-hw-platform"
chmod +x "$failing_bin/omarchy-hw-platform"
! PATH="$failing_bin:$ROOT/bin:$PATH" omarchy-pkg-defaults >/dev/null 2>&1 || fail "a platform the detector cannot tell is refused"
pass "an unknown or undetectable platform is refused"
# omarchy-reinstall-pkgs installs the detected platform's set.
mkdir -p "$work/stubs"
cat >"$work/stubs/omarchy-update-pacman" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$STUB_LOG"
SH
cat >"$work/stubs/omarchy-refresh-pacman" <<'SH'
#!/bin/bash
printf 'refresh %s\n' "$*" >>"$STUB_LOG"
[[ -z ${REFRESH_FAILS:-} ]]
SH
chmod +x "$work/stubs/"*
for platform in aarch64 x86; do
export STUB_LOG="$work/$platform.log"
: >"$STUB_LOG"
OMARCHY_PROC_ROOT="$work/$platform/proc" PATH="$work/stubs:$work/$platform/bin:$ROOT/bin:$PATH" \
omarchy-reinstall-pkgs
expected="-Syu --noconfirm --needed $(tr '\n' ' ' <"$work/$platform.packages")"
[[ $(tail -n 1 "$STUB_LOG") == "${expected% }" ]] ||
fail "$platform: reinstall installs the platform's default set" "$(tail -n 1 "$STUB_LOG")"
done
pass "omarchy-reinstall-pkgs installs the platform's default set"
# Every platform resets to its stable templates, as x86_64 always has; a failed
# refresh stops the reinstall.
reinstall_on() {
local platform=$1
export STUB_LOG="$work/channel.log"
: >"$STUB_LOG"
OMARCHY_PROC_ROOT="$work/$platform/proc" PATH="$work/stubs:$work/$platform/bin:$ROOT/bin:$PATH" \
omarchy-reinstall-pkgs >"$work/channel.out" 2>&1
}
for platform in x86 aarch64 aarch64-apple; do
reinstall_on "$platform" || fail "$platform: reinstall completes" "$(cat "$work/channel.out")"
[[ $(head -n 1 "$STUB_LOG") == "refresh " ]] || fail "$platform: reinstall refreshes on stable" "$(cat "$STUB_LOG")"
expected="-Syu --noconfirm --needed $(tr '\n' ' ' <"$work/$platform.packages")"
[[ $(tail -n 1 "$STUB_LOG") == "${expected% }" ]] || fail "$platform: reinstall installs the platform's set"
done
pass "omarchy-reinstall-pkgs refreshes on stable on every platform"
if REFRESH_FAILS=1 reinstall_on aarch64; then
fail "reinstall stops when the refresh fails"
fi
[[ $(cat "$STUB_LOG") == "refresh " ]] || fail "reinstall runs nothing after a failed refresh" "$(cat "$STUB_LOG")"
pass "omarchy-reinstall-pkgs stops when the refresh fails"
export STUB_LOG="$work/undetected.log"
: >"$STUB_LOG"
if PATH="$failing_bin:$work/stubs:$ROOT/bin:$PATH" omarchy-reinstall-pkgs >/dev/null 2>&1; then
fail "reinstall stops when the platform cannot be told"
fi
! grep -q -- '--needed' "$STUB_LOG" || fail "reinstall installs nothing when the platform cannot be told" "$(cat "$STUB_LOG")"
pass "omarchy-reinstall-pkgs stops when the platform cannot be told"
+146
View File
@@ -0,0 +1,146 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# The two setup leaves and first run call the platform's own setup through the
# real dispatcher: a Mac fixture with omarchy-mac's entrypoints, and x86.
hardware_leaf="$ROOT/install/hardware/platform-setup.sh"
user_leaf="$ROOT/install/user/platform-setup.sh"
[[ $(grep '^run_logged' "$ROOT/install/hardware/all.sh" | tail -n 1) == 'run_logged "$OMARCHY_INSTALL/hardware/platform-setup.sh"' ]] ||
fail "the platform's system setup is the last hardware leaf"
[[ $(grep '^run_logged' "$ROOT/install/user/all.sh" | tail -n 1) == 'run_logged "$OMARCHY_INSTALL/user/platform-setup.sh"' ]] ||
fail "the platform's user setup is the last user leaf"
pass "the platform's setup is the last leaf of hardware and of user setup"
require_platform_fixtures "platform setup on platform fixtures"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
fake_platform "$tmp/apple" aarch64-apple
fake_platform "$tmp/x86" x86
lifecycle=$tmp/lifecycle
mkdir -p "$lifecycle/usr/lib/omarchy/mac" "$lifecycle/usr/lib/omarchy/mac-boot"
for entry in mac/setup-system mac/setup-user mac-boot/setup-boot; do
operation=${entry#*/}
cat >"$lifecycle/usr/lib/omarchy/$entry" <<SH
#!/bin/bash
{ printf '%s' "$operation"; (( \$# == 0 )) || printf ' %s' "\$@"; echo; } >>"$tmp/ran"
[[ ! -e $tmp/fail && ! -e $tmp/fail-$operation ]]
SH
chmod 755 "$lifecycle/usr/lib/omarchy/$entry"
done
chmod -R go-w "$lifecycle"
# Sources a leaf as run_logged does, on a platform fixture.
leaf() {
local platform=$1 script=$2
shift 2
rm -f "$tmp/ran"
env "$@" OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$lifecycle" \
PATH="$tmp/$platform/bin:$ROOT/bin:$PATH" bash -eE -c 'source "$1"' bash "$script" >"$tmp/output" 2>&1
}
# ── system setup ─────────────────────────────────────────────────────────────
leaf apple "$hardware_leaf" || fail "apple: the system setup leaf runs" "$(cat "$tmp/output")"
[[ $(cat "$tmp/ran") == $'setup-boot\nsetup-system' ]] || fail "apple: an install or rerun runs setup-boot, then setup-system, with no argument" "$(cat "$tmp/ran")"
leaf apple "$hardware_leaf" OMARCHY_IMAGE_DEFERRED_HARDWARE=1 || fail "apple: the system setup leaf runs on an image's first boot"
[[ $(cat "$tmp/ran") == $'setup-boot image-first-boot\nsetup-system image-first-boot' ]] ||
fail "apple: an image's first boot tells setup-boot and setup-system so" "$(cat "$tmp/ran")"
touch "$tmp/fail-setup-system"
if leaf apple "$hardware_leaf"; then
fail "apple: a failed setup-system fails the leaf"
fi
[[ $(cat "$tmp/ran") == $'setup-boot\nsetup-system' ]] || fail "apple: setup-system ran after the boot setup and failed" "$(cat "$tmp/ran")"
rm -f "$tmp/fail-setup-system"
touch "$tmp/fail-setup-boot"
if leaf apple "$hardware_leaf"; then
fail "apple: a failed setup-boot fails the leaf"
fi
[[ $(cat "$tmp/ran") == "setup-boot" ]] || fail "apple: setup-system waits for a boot setup that failed" "$(cat "$tmp/ran")"
rm -f "$tmp/fail-setup-boot"
mv "$lifecycle/usr/lib/omarchy/mac-boot/setup-boot" "$tmp/setup-boot.saved"
if leaf apple "$hardware_leaf"; then
fail "apple: a Mac whose boot package lacks setup-boot fails the leaf instead of skipping its boot setup"
fi
grep -q "setup-boot on aarch64-apple needs omarchy-mac-boot" "$tmp/output" || fail "apple: the failure names the boot package" "$(cat "$tmp/output")"
mv "$tmp/setup-boot.saved" "$lifecycle/usr/lib/omarchy/mac-boot/setup-boot"
pass "apple: the system setup leaf runs omarchy-mac-boot's setup-boot, then omarchy-mac's setup-system, says when it is an image's first boot, and fails with either"
touch "$tmp/fail"
leaf x86 "$hardware_leaf" OMARCHY_IMAGE_DEFERRED_HARDWARE=1 || fail "x86: the system setup leaf is a no-op" "$(cat "$tmp/output")"
[[ ! -e $tmp/ran && ! -s $tmp/output ]] || fail "x86: the system setup leaf runs nothing and says nothing" "$(cat "$tmp/output")"
rm -f "$tmp/fail"
pass "x86: the system setup leaf is a no-op, even with Mac entrypoints on disk"
# ── user setup ───────────────────────────────────────────────────────────────
leaf apple "$user_leaf" OMARCHY_IMAGE_ROOT="$tmp/booted" || fail "apple: the user setup leaf runs" "$(cat "$tmp/output")"
[[ $(cat "$tmp/ran") == "setup-user" ]] || fail "apple: the user setup leaf runs setup-user" "$(cat "$tmp/ran" 2>/dev/null)"
touch "$tmp/fail"
if leaf apple "$user_leaf" OMARCHY_IMAGE_ROOT="$tmp/booted"; then
fail "apple: a failed setup-user fails finalization's leaf"
fi
rm -f "$tmp/fail"
mkdir -p "$tmp/image/var/lib/omarchy/image"
: >"$tmp/image/var/lib/omarchy/image/target"
leaf apple "$user_leaf" OMARCHY_IMAGE_ROOT="$tmp/image" || fail "apple: the user setup leaf succeeds in an image build"
[[ ! -e $tmp/ran && $(cat "$tmp/output") == *"waits for first run on the machine"* ]] ||
fail "apple: an image build leaves the platform's user setup to first run" "$(cat "$tmp/output")"
pass "apple: the user setup leaf runs omarchy-mac's setup-user, fails with it, and waits for the machine in an image build"
touch "$tmp/fail"
leaf x86 "$user_leaf" OMARCHY_IMAGE_ROOT="$tmp/booted" || fail "x86: the user setup leaf is a no-op" "$(cat "$tmp/output")"
[[ ! -e $tmp/ran && ! -s $tmp/output ]] || fail "x86: the user setup leaf runs nothing and says nothing" "$(cat "$tmp/output")"
rm -f "$tmp/fail"
pass "x86: the user setup leaf is a no-op, even with Mac entrypoints on disk"
# ── first run ────────────────────────────────────────────────────────────────
# First run on the machine runs setup-user once the session is up. Its failure
# leaves first run pending for the next login, as a failed finalization does.
first_bin=$tmp/first-bin
first_root=$tmp/omarchy
mkdir -p "$first_bin" "$first_root/install/user/first-run" "$tmp/home"
cat >"$first_bin/omarchy-done" <<'SH'
#!/bin/bash
case "$1:$2" in
check:first-run-user) exit 1 ;;
mark:first-run-user) touch "$FIRST_RUN_MARKER" ;;
esac
SH
for helper in omarchy-provision-user omarchy-hook-install omarchy-notification-wait; do
printf '#!/bin/bash\nexit 0\n' >"$first_bin/$helper"
done
chmod +x "$first_bin"/*
for script in welcome.sh wifi.sh enable-user-units.sh gnome-theme.sh gtk-primary-paste.sh audio-tuning.sh; do
printf 'exit 0\n' >"$first_root/install/user/first-run/$script"
done
first_run() {
local platform=$1
rm -f "$tmp/ran" "$tmp/first-run-marker" "$tmp/home/.local/state/omarchy/first-run.log"
HOME="$tmp/home" OMARCHY_PATH="$first_root" FIRST_RUN_MARKER="$tmp/first-run-marker" \
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$lifecycle" \
PATH="$first_bin:$tmp/$platform/bin:$ROOT/bin:$PATH" bash "$ROOT/bin/omarchy-provision-first-run" >"$tmp/output" 2>&1
}
first_run apple || fail "apple: first run finishes" "$(cat "$tmp/output")"
[[ $(cat "$tmp/ran") == "setup-user" && -e $tmp/first-run-marker ]] ||
fail "apple: first run runs setup-user and completes" "$(cat "$tmp/ran" 2>/dev/null)"
touch "$tmp/fail"
first_run apple || fail "apple: first run with a failing setup-user still runs its other steps"
[[ ! -e $tmp/first-run-marker ]] || fail "apple: a failed setup-user keeps first run pending"
grep -q "Failed: run the platform's user setup" "$tmp/home/.local/state/omarchy/first-run.log" ||
fail "apple: first run logs the failed platform setup" "$(cat "$tmp/home/.local/state/omarchy/first-run.log")"
first_run x86 || fail "x86: first run finishes"
[[ ! -e $tmp/ran && -e $tmp/first-run-marker ]] || fail "x86: first run runs no platform setup and completes"
rm -f "$tmp/fail"
pass "first run runs the platform's user setup, and a failure keeps first run pending"
@@ -0,0 +1,313 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# First-boot setup's keyboard step. The owner types the disk password at boot in
# the layout /etc/vconsole.conf names, so a layout that did not load, did not
# persist, or does not read back must be asked for again before the password
# form. keyboard_form and apply_keyboard run as omarchy-provision-owner defines
# them, against a fixture root, with loadkeys, localectl and systemd-firstboot
# faked.
tmp=$(cd -- "$(mktemp -d)" && pwd -P)
trap 'rm -rf "$tmp"' EXIT
root=$tmp/root
stub_bin=$tmp/bin
conf=$root/etc/vconsole.conf
mkdir -p "$stub_bin"
# Each fake takes its next behaviour from a queue file, and behaves normally
# once the queue is empty.
cat >"$stub_bin/next-mode" <<'SH'
#!/bin/bash
queue=$1
[[ -s $queue ]] || { echo normal; exit 0; }
head -n1 "$queue"
sed -i 1d "$queue"
SH
cat >"$stub_bin/tty" <<SH
#!/bin/bash
[[ -e "$tmp/no-tty" ]] && { echo "not a tty"; exit 1; }
echo /dev/tty1
SH
cat >"$stub_bin/loadkeys" <<SH
#!/bin/bash
echo "loadkeys \$*" >>"$tmp/calls"
[[ \$("$stub_bin/next-mode" "$tmp/loadkeys-modes") == normal ]] || { echo "cannot open file \$1" >&2; exit 1; }
SH
cat >"$stub_bin/localectl" <<SH
#!/bin/bash
args=("\$@")
[[ \${args[0]} == --no-pager ]] && args=("\${args[@]:1}")
case \${args[0]} in
list-keymaps)
[[ \$("$stub_bin/next-mode" "$tmp/list-modes") == normal ]] || { echo "Failed to connect to bus" >&2; exit 1; }
printf '%s\n' bg-cp1251 colemak cz de-latin1 dk fr pl ua us
;;
set-keymap)
echo "localectl set-keymap \${args[1]}" >>"$tmp/calls"
[[ \$("$stub_bin/next-mode" "$tmp/localectl-modes") == normal ]] || exit 1
printf 'KEYMAP=%s\nXKBLAYOUT=%s\n' "\${args[1]}" "\${args[1]}" >"$conf"
;;
*) exit 1 ;;
esac
SH
cat >"$stub_bin/systemd-firstboot" <<SH
#!/bin/bash
keymap=\${1#--keymap=}
echo "systemd-firstboot \$*" >>"$tmp/calls"
case \$("$stub_bin/next-mode" "$tmp/firstboot-modes") in
normal) printf 'KEYMAP=%s\nXKBLAYOUT=%s\nXKBMODEL=pc105\n' "\$keymap" "\$keymap" >"$conf" ;;
nothing) ;;
mismatch) printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$conf" ;;
no-xkb) printf 'KEYMAP=%s\n' "\$keymap" >"$conf" ;;
fail) echo "Failed to write vconsole.conf" >&2; exit 1 ;;
esac
SH
chmod +x "$stub_bin"/*
export PATH="$stub_bin:$PATH"
sed -n '/^keyboard_form() {/,/^}/p; /^keyboard_xkb_settings() {/,/^}/p; /^apply_keyboard() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner" |
sed "s|/etc/|$root/etc/|g" >"$tmp/keyboard.sh"
grep -q '^keyboard_form() {' "$tmp/keyboard.sh" && grep -q '^apply_keyboard() {' "$tmp/keyboard.sh" ||
fail "omarchy-provision-owner defines the keyboard step"
# The owner's answers, one "label|keymap" per prompt. Running out means the step
# asked once more than the test answered.
cat >"$tmp/form.sh" <<SH
set -euo pipefail
source "$tmp/keyboard.sh"
LOG_FILE=$tmp/log
OMARCHY_FORM_BACK=2
step() { echo "prompt" >>"$tmp/screen"; }
notice() { echo "notice: \$1" >>"$tmp/screen"; }
log_step() { echo "\$1" >>"\$LOG_FILE"; }
confirm_reboot() { exit 5; }
omarchy_prompt_keyboard() {
local answer
answer=\$("$stub_bin/next-mode" "$tmp/answers")
[[ \$answer != normal ]] || { echo "asked again" >>"$tmp/screen"; exit 9; }
keyboard_label=\${answer%%|*}
keyboard=\${answer#*|}
}
keyboard_form
echo "password form" >>"$tmp/screen"
SH
# A fresh deferred install: the placeholder US layout from the image build.
fresh_root() {
rm -rf "$root" "$tmp"/*-modes "$tmp/answers" "$tmp/calls" "$tmp/screen" "$tmp/log" "$tmp/no-tty"
mkdir -p "$root/etc"
printf 'KEYMAP=us\nXKBLAYOUT=us\n' >"$conf"
: >"$tmp/calls"
: >"$tmp/log"
}
answers() {
printf '%s\n' "$@" >"$tmp/answers"
}
form() {
local status=0
bash "$tmp/form.sh" || status=$?
return "$status"
}
prompts() {
grep -cx prompt "$tmp/screen" || true
}
assert_layout() {
grep -qx "KEYMAP=$1" "$conf" && grep -q '^XKBLAYOUT=.' "$conf" || fail "$2: vconsole.conf holds $1" "$(cat "$conf")"
}
# Asked once more after the failure, with the reason on the screen and in the
# log, and only the second answer reaches the password form.
assert_asked_again() {
local context=$1 log_line=$2
[[ $(prompts) == 2 ]] || fail "$context: the keyboard is asked for again" "$(cat "$tmp/screen")"
[[ $(sed -n 2p "$tmp/screen") == "notice: Could not set the Danish keyboard layout. Choose it again." ]] ||
fail "$context: the owner is told the layout did not take" "$(cat "$tmp/screen")"
[[ $(tail -n1 "$tmp/screen") == "password form" && $(grep -c 'password form' "$tmp/screen") == 1 ]] ||
fail "$context: the password form comes only after a layout took" "$(cat "$tmp/screen")"
grep -q "$log_line" "$tmp/log" || fail "$context: the log records the failure" "$(cat "$tmp/log")"
}
fresh_root
answers 'Danish|dk'
form || fail "a known layout is set" "$(cat "$tmp/screen" "$tmp/log")"
[[ $(prompts) == 1 ]] && ! grep -q '^notice' "$tmp/screen" || fail "a layout that takes is asked for once" "$(cat "$tmp/screen")"
grep -qx 'loadkeys dk' "$tmp/calls" && grep -qx 'systemd-firstboot --keymap=dk --force' "$tmp/calls" ||
fail "the layout is loaded on the console and persisted" "$(cat "$tmp/calls")"
assert_layout dk "a known layout"
pass "a layout that loads, persists and reads back goes straight to the password form"
# systemd-firstboot writes only KEYMAP= for a keymap kbd-model-map lacks, as it
# does for Polish, Ukrainian and Colemak. Setup supplies the XKB layout.
fresh_root
answers 'Polish|pl'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "a keymap firstboot has no XKB layout for is set" "$(cat "$tmp/screen" "$tmp/log")"
[[ $(prompts) == 1 ]] && ! grep -q '^notice' "$tmp/screen" || fail "Polish is asked for once" "$(cat "$tmp/screen")"
grep -qx 'XKBLAYOUT=pl' "$conf" || fail "Polish gets the pl XKB layout" "$(cat "$conf")"
fresh_root
answers 'Ukrainian|ua'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "Ukrainian is set" "$(cat "$tmp/screen" "$tmp/log")"
grep -qx 'KEYMAP=ua' "$conf" && grep -qx 'XKBLAYOUT=ua,us' "$conf" &&
grep -qx 'XKBOPTIONS=terminate:ctrl_alt_bksp,grp:shifts_toggle,grp_led:scroll' "$conf" ||
fail "a non-Latin layout keeps a US layout to switch to" "$(cat "$conf")"
fresh_root
answers 'English (US, Colemak)|colemak'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "Colemak is set" "$(cat "$tmp/screen" "$tmp/log")"
grep -qx 'XKBLAYOUT=us' "$conf" && grep -qx 'XKBVARIANT=colemak' "$conf" ||
fail "Colemak is the us layout's colemak variant" "$(cat "$conf")"
# The desktop must type what the console does, or the shared password differs:
# kbd's cz is QWERTY and bg-cp1251 phonetic, unlike XKB's defaults.
fresh_root
answers 'Czech|cz'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "Czech is set" "$(cat "$tmp/screen" "$tmp/log")"
grep -qx 'XKBLAYOUT=cz,us' "$conf" && grep -qx 'XKBVARIANT=qwerty,' "$conf" ||
fail "Czech is QWERTY on the desktop as on the console" "$(cat "$conf")"
fresh_root
answers 'Bulgarian|bg-cp1251'
echo no-xkb >"$tmp/firstboot-modes"
form || fail "Bulgarian is set" "$(cat "$tmp/screen" "$tmp/log")"
grep -qx 'XKBLAYOUT=bg,us' "$conf" && grep -qx 'XKBVARIANT=phonetic,' "$conf" ||
fail "Bulgarian is phonetic on the desktop as on the console" "$(cat "$conf")"
pass "a keymap systemd-firstboot has no XKB layout for gets one from setup"
# Every layout the form offers must end with an XKB layout: kbd-model-map's, or
# setup's own. A keymap in neither could never finish the keyboard step.
model_map=/usr/share/systemd/kbd-model-map
if [[ -r $model_map ]]; then
source "$ROOT/install/provisioning/setup-form.sh"
source "$tmp/keyboard.sh"
while IFS='|' read -r label keymap; do
awk -v k="$keymap" '$1 == k { found = 1 } END { exit !found }' "$model_map" ||
{ declare -F keyboard_xkb_settings >/dev/null && keyboard_xkb_settings "$keymap" >/dev/null; } ||
fail "the $label layout ($keymap) gets an XKB layout"
done <<<"$OMARCHY_KEYBOARD_LAYOUTS"
pass "every layout the form offers gets an XKB layout"
else
echo "ok - # SKIP no $model_map to check the form's layouts against"
fi
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo fail >"$tmp/loadkeys-modes"
form || fail "loadkeys failing once, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "loadkeys fails" "loadkeys could not load keymap dk"
[[ $(grep -c '^systemd-firstboot' "$tmp/calls") == 1 ]] || fail "a layout the console refused is not persisted" "$(cat "$tmp/calls")"
assert_layout dk "after loadkeys failed once"
pass "a layout loadkeys refuses is asked for again"
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo nothing >"$tmp/firstboot-modes"
form || fail "firstboot writing nothing, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "firstboot writes nothing" "does not read back as keymap dk"
assert_layout dk "after firstboot wrote nothing once"
pass "a systemd-firstboot that writes nothing is asked for again"
for mode in mismatch no-xkb; do
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo "$mode" >"$tmp/firstboot-modes"
form || fail "readback $mode, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "readback $mode" "does not read back as keymap dk"
assert_layout dk "after readback $mode"
done
pass "a vconsole.conf that reads back another keymap, or no XKB layout, is asked for again"
# The readback runs in a clean shell: values this process exports never stand
# in for the file.
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo nothing >"$tmp/firstboot-modes"
: >"$conf"
KEYMAP=dk XKBLAYOUT=dk form || fail "an exported layout, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "exported KEYMAP" "does not read back as keymap dk"
pass "the readback ignores KEYMAP and XKBLAYOUT in the environment"
fresh_root
answers 'Danish|xx' 'Danish|dk'
form || fail "an unknown keymap, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "unknown keymap" "keymap xx unknown to localectl"
! grep -q 'keymap=xx\|set-keymap xx' "$tmp/calls" || fail "an unknown keymap is never persisted" "$(cat "$tmp/calls")"
assert_layout dk "after an unknown keymap"
pass "a keymap localectl does not know is asked for again rather than keeping the default"
fresh_root
answers 'Danish|DK' 'Danish|dk'
form || fail "a keymap in the wrong case, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "keymap case" "keymap DK unknown to localectl"
assert_layout dk "after a keymap in the wrong case"
pass "a keymap matches localectl's list exactly, case included"
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo fail >"$tmp/list-modes"
form || fail "localectl failing to list keymaps, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "localectl list fails" "localectl could not list keymaps to check dk"
[[ $(grep -c '^systemd-firstboot' "$tmp/calls") == 1 ]] || fail "a keymap that could not be checked is not persisted" "$(cat "$tmp/calls")"
pass "a keymap list localectl cannot produce is a failure, logged apart from an unknown keymap"
fresh_root
answers 'Danish|dk'
echo fail >"$tmp/firstboot-modes"
form || fail "localectl persists when firstboot fails" "$(cat "$tmp/screen" "$tmp/log")"
[[ $(prompts) == 1 ]] && grep -qx 'localectl set-keymap dk' "$tmp/calls" || fail "the localectl fallback persists the layout" "$(cat "$tmp/calls")"
assert_layout dk "the localectl fallback"
pass "a failed systemd-firstboot falls back to localectl, checked the same way"
fresh_root
answers 'Danish|dk' 'Danish|dk'
echo fail >"$tmp/firstboot-modes"
echo fail >"$tmp/localectl-modes"
form || fail "both persisting paths failing, then the retry" "$(cat "$tmp/screen" "$tmp/log")"
assert_asked_again "firstboot and localectl fail" "could not persist keymap dk"
pass "a layout neither systemd-firstboot nor localectl persists is asked for again"
# A layout that never takes keeps the owner at the keyboard step.
fresh_root
answers 'Danish|dk' 'Danish|dk'
printf 'fail\nfail\n' >"$tmp/loadkeys-modes"
if form; then fail "a layout that never loads does not end the keyboard step"; fi
[[ $(tail -n1 "$tmp/screen") == "asked again" ]] && ! grep -q 'password form' "$tmp/screen" ||
fail "the password form is never reached while the layout fails" "$(cat "$tmp/screen")"
grep -qx 'KEYMAP=us' "$conf" || fail "the install's layout is left alone" "$(cat "$conf")"
pass "while the layout fails the owner never reaches the password form"
# A machine that can't set any layout (localectl gone for good) must not keep the
# owner at the keyboard step: after three failures in a row the attempt fails,
# and setup's own retry-or-console screen takes over.
fresh_root
answers 'Danish|dk' 'German|de-latin1' 'Danish|dk' 'Danish|dk'
printf 'fail\nfail\nfail\nfail\n' >"$tmp/list-modes"
status=0
form || status=$?
(( status == 1 )) || fail "a layout that can never be set fails the attempt" "status $status: $(cat "$tmp/screen")"
[[ $(prompts) == 3 ]] && [[ $(tail -n1 "$tmp/screen") == "notice: Setup could not set a keyboard layout on this machine." ]] ||
fail "the keyboard step gives up after three failures, and says so" "$(cat "$tmp/screen")"
! grep -q 'password form' "$tmp/screen" || fail "a layout that can never be set never reaches the password form" "$(cat "$tmp/screen")"
grep -q 'no keyboard layout could be set after 3 attempts' "$tmp/log" || fail "the log records giving up" "$(cat "$tmp/log")"
sed -n '/^run_setup() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner" | grep -qx ' keyboard_form || return 1' ||
fail "a failed keyboard step fails the setup attempt, which offers retry or a console"
pass "a machine that can't set any layout fails the attempt after three tries, reaching setup's retry-or-console screen"
fresh_root
answers 'Danish|dk'
touch "$tmp/no-tty"
form || fail "off a console the layout is only persisted" "$(cat "$tmp/screen" "$tmp/log")"
! grep -q '^loadkeys' "$tmp/calls" || fail "loadkeys runs only on a virtual console" "$(cat "$tmp/calls")"
assert_layout dk "off a console"
pass "off a virtual console the layout is persisted and checked without loadkeys"
+128
View File
@@ -0,0 +1,128 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# A deferred install (an Apple Silicon image, an OEM ISO install) runs its
# hardware steps on the UTC placeholder, so the wireless regulatory domain can
# only come from the timezone the owner picks in first-boot setup. The setup
# step runs against a fixture root: the real install step with its paths moved
# there, and wireless-regdb's boot helper as it ships, with iw recording calls.
tmp=$(cd -- "$(mktemp -d)" && pwd -P)
trap 'rm -rf "$tmp"' EXIT
root=$tmp/root
omarchy=$tmp/omarchy
stub_bin=$tmp/bin
conf=$root/etc/conf.d/wireless-regdom
calls=$tmp/calls
mkdir -p "$stub_bin" "$omarchy/bin" "$omarchy/install/hardware"
ln -s "$ROOT/bin/omarchy-cmd-present" "$omarchy/bin/omarchy-cmd-present"
sed -e "s|/etc/|$root/etc/|g" -e "s|/usr/share/zoneinfo|$root/usr/share/zoneinfo|g" \
"$ROOT/install/hardware/set-wireless-regdom.sh" >"$omarchy/install/hardware/set-wireless-regdom.sh"
cat >"$stub_bin/timedatectl" <<SH
#!/bin/bash
[[ \$1 == set-timezone && -f "$root/usr/share/zoneinfo/\$2" ]] || exit 1
ln -sfn "../usr/share/zoneinfo/\$2" "$root/etc/localtime"
SH
printf '#!/bin/bash\nexec "$@"\n' >"$stub_bin/sudo"
cat >"$stub_bin/set-wireless-regdom" <<SH
#!/bin/bash
unset WIRELESS_REGDOM
. "$conf"
[ -n "\${WIRELESS_REGDOM}" ] && iw reg set \${WIRELESS_REGDOM}
SH
cat >"$stub_bin/iw" <<SH
#!/bin/bash
echo "iw \$*" >>"$calls"
[[ ! -e "$tmp/no-wifi" ]] || { echo "nl80211 not found." >&2; exit 1; }
SH
chmod +x "$stub_bin"/*
# A fresh deferred install: wireless-regdb's commented list, the UTC placeholder.
fresh_root() {
rm -rf "$root" "$calls" "$tmp/no-wifi"
mkdir -p "$root/etc/conf.d" "$root/usr/share/zoneinfo/Australia" "$root/usr/share/zoneinfo/America"
printf '#\n# Wireless regulatory domain configuration\n#\n#WIRELESS_REGDOM="AU"\n#WIRELESS_REGDOM="US"\n' >"$conf"
: >"$root/usr/share/zoneinfo/UTC"
: >"$root/usr/share/zoneinfo/Australia/Brisbane"
: >"$root/usr/share/zoneinfo/America/New_York"
printf 'AU\t-2728+15302\tAustralia/Brisbane\tQueensland (most areas)\nUS\t+404251-0740023\tAmerica/New_York\tEastern (most areas)\n' \
>"$root/usr/share/zoneinfo/zone.tab"
ln -s ../usr/share/zoneinfo/UTC "$root/etc/localtime"
: >"$calls"
}
export PATH="$stub_bin:$omarchy/bin:$PATH"
export OMARCHY_PATH=$omarchy
# The two setup functions as omarchy-provision-owner defines them, with its
# fixed paths moved into the fixture root.
sed -n '/^configure_timezone() {/,/^}/p; /^configure_wireless_regdom() {/,/^}/p' "$ROOT/bin/omarchy-provision-owner" |
sed -e "s|/usr/share/zoneinfo|$root/usr/share/zoneinfo|g" -e "s|/etc/|$root/etc/|g" >"$tmp/setup.sh"
grep -q '^configure_wireless_regdom() {' "$tmp/setup.sh" || fail "omarchy-provision-owner sets the regulatory domain in setup"
# shellcheck disable=SC1091
source "$tmp/setup.sh"
regdom_lines() {
grep '^WIRELESS_REGDOM=' "$conf" || true
}
# The install step itself stays as an up-front install runs it: persisted only,
# the radios untouched until the reboot that follows an install.
fresh_root
ln -sfn ../usr/share/zoneinfo/America/New_York "$root/etc/localtime"
bash "$omarchy/install/hardware/set-wireless-regdom.sh"
[[ $(regdom_lines) == 'WIRELESS_REGDOM="US"' ]] || fail "an up-front install persists its timezone's country" "$(cat "$conf")"
[[ ! -s $calls ]] || fail "an up-front install leaves the radios alone" "$(cat "$calls")"
pass "an up-front install persists its timezone's country and leaves the radios to the reboot"
fresh_root
bash "$omarchy/install/hardware/set-wireless-regdom.sh"
[[ -z $(regdom_lines) ]] || fail "the UTC placeholder names no country" "$(cat "$conf")"
pass "a deferred install's hardware step sets nothing from the UTC placeholder"
timezone=Australia/Brisbane
configure_timezone >/dev/null
[[ $(readlink "$root/etc/localtime") == ../usr/share/zoneinfo/Australia/Brisbane ]] ||
fail "setup applies the owner's timezone"
[[ $(regdom_lines) == 'WIRELESS_REGDOM="AU"' ]] || fail "setup persists the owner's country" "$(cat "$conf")"
[[ $(cat "$calls") == "iw reg set AU" ]] || fail "setup applies the owner's country to the radios" "$(cat "$calls")"
pass "setup persists the owner's timezone's country and applies it without a reboot"
configure_timezone >/dev/null
[[ $(regdom_lines) == 'WIRELESS_REGDOM="AU"' ]] || fail "a retried setup writes the domain once" "$(cat "$conf")"
pass "a retried setup keeps one regulatory domain line"
fresh_root
printf 'WIRELESS_REGDOM="DE"\n' >>"$conf"
timezone=America/New_York
configure_timezone >/dev/null
[[ $(regdom_lines) == 'WIRELESS_REGDOM="DE"' ]] || fail "setup keeps a domain already chosen" "$(cat "$conf")"
[[ $(cat "$calls") == "iw reg set DE" ]] || fail "setup applies the domain already chosen" "$(cat "$calls")"
pass "a regulatory domain already chosen is kept"
fresh_root
timezone=UTC
configure_timezone >/dev/null
[[ -z $(regdom_lines) ]] || fail "a timezone without a country sets no domain" "$(cat "$conf")"
[[ ! -s $calls ]] || fail "a timezone without a country leaves the radios alone" "$(cat "$calls")"
pass "a timezone without a country sets no regulatory domain"
fresh_root
: >"$tmp/no-wifi"
timezone=America/New_York
configure_timezone >/dev/null || fail "setup goes on on a machine without Wi-Fi"
[[ $(regdom_lines) == 'WIRELESS_REGDOM="US"' ]] || fail "a machine without Wi-Fi still gets the domain for later" "$(cat "$conf")"
pass "a machine without Wi-Fi finishes setup with the domain persisted for its next boot"
fresh_root
timezone=Omarchy/Nowhere
configure_timezone >/dev/null || fail "setup goes on after a timezone it cannot apply"
[[ -z $(regdom_lines) && ! -s $calls ]] || fail "a timezone that did not apply sets no domain" "$(cat "$conf" "$calls")"
pass "a timezone that could not be applied sets no regulatory domain"
@@ -0,0 +1,92 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
TMPDIR=""
QS_PID=""
cleanup() {
if [[ -n $QS_PID ]] && kill -0 "$QS_PID" 2>/dev/null; then
kill "$QS_PID" 2>/dev/null || true
wait "$QS_PID" 2>/dev/null || true
fi
if [[ -n $TMPDIR && -f $TMPDIR/stub.pid ]]; then
kill "$(<"$TMPDIR/stub.pid")" 2>/dev/null || true
fi
if [[ -n $TMPDIR && -d $TMPDIR ]]; then
rm -rf "$TMPDIR"
fi
}
trap cleanup EXIT
require_compositor "remote session service test"
if ! command -v quickshell >/dev/null 2>&1; then
skip "quickshell not installed; skipping remote session service test"
exit 0
fi
require_command jq
require_command grim
TMPDIR=$(mktemp -d)
result="$TMPDIR/result.json"
log="$TMPDIR/quickshell.log"
config_dir="$TMPDIR/remote-session"
mkdir -p "$config_dir" "$TMPDIR/home" "$TMPDIR/bin"
cp "$SHELL_TEST_DIR/fixtures/remote-session/shell.qml" "$config_dir/shell.qml"
# A stand-in for the real server: the probe matches it by process name, so it
# must stay a script called gliff-server rather than exec into sleep, and it
# takes its sleep down with it when killed. Its ssh peer comes from the
# environment the fixture launches it with. It cannot capture the screen, so
# the fixture uses grim for the screencast event.
cat >"$TMPDIR/bin/gliff-server" <<'SH'
#!/bin/bash
sleep 30 &
trap 'kill $!' TERM EXIT
wait $!
SH
chmod +x "$TMPDIR/bin/gliff-server"
OMARCHY_PATH="$ROOT" \
OMARCHY_QML_TEST_RESULT="$result" \
OMARCHY_QML_TEST_STUB_PID="$TMPDIR/stub.pid" \
OMARCHY_QML_TEST_FRAME="$TMPDIR/frame.png" \
HOME="$TMPDIR/home" \
XDG_CONFIG_HOME="$TMPDIR/home/.config" \
XDG_CACHE_HOME="$TMPDIR/home/.cache" \
XDG_STATE_HOME="$TMPDIR/home/.local/state" \
QML2_IMPORT_PATH="$ROOT/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \
QML_IMPORT_PATH="$ROOT/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \
PATH="$TMPDIR/bin:$ROOT/bin:$PATH" \
quickshell -p "$config_dir" --no-color >"$log" 2>&1 &
QS_PID=$!
for _ in {1..150}; do
[[ -s $result ]] && break
if ! kill -0 "$QS_PID" 2>/dev/null; then
sed -n '1,160p' "$log" >&2
fail "remote session quickshell exited before writing result"
fi
sleep 0.1
done
[[ -s $result ]] || {
sed -n '1,160p' "$log" >&2
fail "remote session service test timed out"
}
if ! jq -e '.ok == true' "$result" >/dev/null; then
jq . "$result" >&2
sed -n '1,160p' "$log" >&2
fail "remote session service tracks a gliff-server session"
fi
pass "remote session service tracks a gliff-server session"
[[ -s $TMPDIR/frame.png ]] || fail "the fixture raised screencast events through grim"
pass "the fixture raised screencast events through grim"
+88
View File
@@ -0,0 +1,88 @@
#!/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 model = requireFromRoot('shell/plugins/services/remote-session/RemoteSessionModel.js')
assert(model.isCaptureEvent('screencast'), 'the screencast event triggers a probe')
assert(!model.isCaptureEvent('screencastv2'), 'the paired screencastv2 event does not double the probe')
assert(!model.isCaptureEvent('openwindow') && !model.isCaptureEvent(''), 'unrelated Hyprland events are ignored')
assertDeepEqual(model.stateFromOutput(''), { active: false, peers: [] }, 'no gliff-server means no session')
assertDeepEqual(model.stateFromOutput('\n'), { active: false, peers: [] }, 'blank output means no session')
assertDeepEqual(model.stateFromOutput('4321 \n'), { active: true, peers: [] }, 'a local server without ssh is a session with no known peer')
assertDeepEqual(model.stateFromOutput('4321 10.0.0.5\n'), { active: true, peers: ['10.0.0.5'] }, 'an ssh-spawned server reports the client address')
assertDeepEqual(model.stateFromOutput('1 10.0.0.5\n2 10.0.0.5\n3 fd00::9\n'), { active: true, peers: ['10.0.0.5', 'fd00::9'] }, 'peers are listed once each')
const service = read('shell/plugins/services/remote-session/Service.qml')
const manifest = JSON.parse(read('shell/plugins/services/remote-session/manifest.json'))
assertEqual(manifest.id, 'omarchy.remote-session', 'service manifest id matches the indicator lookup')
assert(manifest.kinds.includes('service') && manifest.entryPoints.service === 'Service.qml', 'service manifest declares a service entry point')
assert(service.includes('target: Hyprland') && service.includes('isCaptureEvent(event ? event.name : "")'), 'service re-probes on Hyprland screencast events')
assert(service.includes('eventDebounce.restart()'), 'service debounces event bursts into one probe')
assert(service.includes('/shell/plugins/services/remote-session/probe.sh"'), 'service runs the shared probe script')
assert(service.includes('running: root.active') && service.includes('interval: 2000'), 'service polls only while a session is active, since the stop event is not reliable')
assert(!service.includes('omarchy-notification-send'), 'a session is shown by the indicator alone, with no notification')
const indicator = read('shell/plugins/bar/indicators/RemoteSession.qml')
const widget = read('shell/plugins/bar/widgets/Indicators.qml')
const widgetManifest = JSON.parse(read('shell/plugins/bar/widgets/Indicators.manifest.json'))
assert(widget.match(/defaultIndicatorEntries: \[ "PasswordlessSudo", "ScreenRecording", "RemoteSession"/), 'remote session is included in the default indicator tray')
assert(widgetManifest.barWidget.schema.find(field => field.key === 'items').options.some(option => option.value === 'RemoteSession'), 'remote session is configurable alongside the other indicators')
assert(indicator.includes('firstPartyServiceFor("omarchy.remote-session")'), 'indicator reads the remote session service')
assert(indicator.includes('"Remote session from " + peers.join(", ")'), 'active tooltip names the connected peers')
assert(indicator.includes('useActiveColor: true') && indicator.includes('activeColor: Commons.Color.urgent'), 'active remote session uses the theme danger color')
assertEqual(indicator.match(/activeText: "([^"]+)"/)[1], indicator.match(/inactiveText: "([^"]+)"/)[1], 'remote session keeps the same icon in both states')
assert(!indicator.includes('visible:'), 'remote session uses the shared indicator visibility and hover behavior')
assert(indicator.includes('root.remoteSessionService.refresh()'), 'indicator refresh re-probes the service')
const shell = read('shell/shell.qml')
assert((shell.match(/"omarchy\.remote-session"/g) || []).length >= 2, 'third-party bar clones can read the remote session service')
const api = read('shell/services/PluginFirstPartyServiceApi.qml')
assert(api.includes('property bool active: false') && api.includes('property var peers: []'), 'service proxy exposes the remote session state')
assert(api.includes('serviceId === "omarchy.remote-session" && _refresh') && shell.includes('_refresh: function()'), 'service proxy forwards refresh for cloned indicator widgets')
JS
TMPDIR=$(mktemp -d)
trap 'rm -rf "$TMPDIR"' EXIT
mkdir -p "$TMPDIR/bin" "$TMPDIR/proc/100" "$TMPDIR/proc/200"
probe_script="$ROOT/shell/plugins/services/remote-session/probe.sh"
bash -n "$probe_script" || fail "remote session probe is valid bash"
pass "remote session probe is valid bash"
cat >"$TMPDIR/bin/pgrep" <<'SH'
#!/bin/bash
[[ $1 == "-x" && $2 == "-u" && $3 == "$(id -u)" && $4 == "gliff-server" ]] || exit 2
[[ -n ${GLIFF_PIDS:-} ]] || exit 1
printf '%s\n' $GLIFF_PIDS
SH
chmod +x "$TMPDIR/bin/pgrep"
# The probe reads /proc/<pid>/environ; point a copy at fixtures instead.
sed "s|/proc/\$pid/environ|$TMPDIR/proc/\$pid/environ|" "$probe_script" >"$TMPDIR/probe.sh"
grep -q "$TMPDIR/proc" "$TMPDIR/probe.sh" || fail "probe reads the server environment from /proc"
pass "probe reads the server environment from /proc"
probe() {
PATH="$TMPDIR/bin:$PATH" GLIFF_PIDS="${1:-}" bash "$TMPDIR/probe.sh"
}
[[ -z $(probe) ]] || fail "probe prints nothing without a gliff server"
pass "probe prints nothing without a gliff server"
printf 'HOME=/home/x\0SSH_CONNECTION=10.0.0.5 51234 10.0.0.1 22\0TERM=foot\0' >"$TMPDIR/proc/100/environ"
printf 'HOME=/home/x\0TERM=foot\0' >"$TMPDIR/proc/200/environ"
[[ $(probe "100") == "100 10.0.0.5" ]] || fail "probe reports the ssh client address of a spawned server"
pass "probe reports the ssh client address of a spawned server"
[[ $(probe "200") == "200 " ]] || fail "probe reports a local server with an empty peer"
pass "probe reports a local server with an empty peer"
(( $(probe "100 200" | wc -l) == 2 )) || fail "probe lists every server"
pass "probe lists every server"
+228
View File
@@ -0,0 +1,228 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
tmp=$(mktemp -d)
cleanup() {
for child in $(jobs -pr); do kill "$child" 2>/dev/null || true; done
if [[ -f $tmp/recorder-pid ]]; then kill "$(<"$tmp/recorder-pid")" 2>/dev/null || true; fi
rm -rf "$tmp"
}
trap cleanup EXIT
mkdir -p "$tmp/bin" "$tmp/runtime" "$tmp/recordings"
mkfifo "$tmp/release"
cat >"$tmp/bin/omarchy-capture-screenrecording-process" <<'SH'
#!/bin/bash
if [[ ${1:-} != "--pid" || $2 == "123" ]]; then
if [[ ${TEST_ROLE:-} == "old-status" && ${2:-} == "123" ]]; then
touch "$TEST_STATE/checked"
read -r _ <"$TEST_STATE/release"
fi
exit 1
fi
[[ -f $TEST_STATE/recorder-pid && $2 == "$(<"$TEST_STATE/recorder-pid")" ]] || exit 1
if [[ ${3:-} == "--signal" ]]; then kill -TERM "$2"; else kill -0 "$2" 2>/dev/null; fi
SH
cat >"$tmp/bin/wf-recorder" <<'SH'
#!/bin/bash
echo "$$" >"$TEST_STATE/recorder-pid"
for arg in "$@"; do
if [[ ${next:-} == 1 ]]; then [[ -n ${TEST_NEVER_READY:-} ]] || touch "$arg"; break; fi
[[ $arg == "-f" ]] && next=1
done
if [[ -n ${TEST_IGNORE_TERM:-} ]]; then trap '' TERM; else trap 'exit 0' TERM INT; fi
while true; do sleep 0.05; done
SH
cat >"$tmp/bin/pactl" <<'SH'
#!/bin/bash
case $1 in
get-default-sink) echo desktop ;;
get-default-source) echo microphone ;;
load-module)
id=$(<"$TEST_STATE/next-module")
echo $(( id + 1 )) >"$TEST_STATE/next-module"
touch "$TEST_STATE/module-$id"
echo "$id"
;;
unload-module) rm -f "$TEST_STATE/module-$2" ;;
esac
SH
cat >"$tmp/bin/flock" <<'SH'
#!/bin/bash
[[ ${TEST_ROLE:-} == "new-start" ]] && touch "$TEST_STATE/lock-attempted"
exec /usr/bin/flock "$@"
SH
cat >"$tmp/bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
[[ $1 == "wf-recorder" ]]
SH
cat >"$tmp/bin/omarchy-hyprland-monitor-focused" <<'SH'
#!/bin/bash
echo Virtual-1
SH
cat >"$tmp/bin/ffmpeg" <<'SH'
#!/bin/bash
if [[ ${TEST_ROLE:-} == "old-stop" && ! -e $TEST_STATE/checked ]]; then
touch "$TEST_STATE/checked"
read -r _ <"$TEST_STATE/release"
fi
touch "${@: -3:1}"
SH
for command in omarchy-shell omarchy-notification-send ffprobe pkill; do
printf '#!/bin/bash\nexit 0\n' >"$tmp/bin/$command"
done
cat >"$tmp/bin/omarchy-shell" <<'SH'
#!/bin/bash
# A stop that finds no start pending while the start is finishing.
if [[ -n ${TEST_LATE_STOP:-} && ! -e $TEST_STATE/late-stop-sent ]]; then
touch "$TEST_STATE/late-stop-sent"
rm -f "$XDG_RUNTIME_DIR/omarchy-screenrecord-starting"
TEST_LATE_STOP= "$TEST_RECORD" --stop-recording 9>&- >/dev/null 2>&1 &
echo $! >"$TEST_STATE/late-stop-pid"
until [[ -e $XDG_RUNTIME_DIR/omarchy-screenrecord-cancel ]]; do sleep 0.05; done
fi
if [[ -n ${TEST_PAUSE_INDICATOR:-} && ! -e $TEST_STATE/indicator-paused ]]; then
touch "$TEST_STATE/indicator-paused"
read -r _ <"$TEST_STATE/release"
fi
exit 0
SH
chmod +x "$tmp/bin/"*
export PATH="$tmp/bin:$ROOT/bin:$PATH" TEST_STATE="$tmp" XDG_RUNTIME_DIR="$tmp/runtime"
export OMARCHY_SCREENRECORD_DIR="$tmp/recordings"
record="$ROOT/bin/omarchy-capture-screenrecording"
export TEST_RECORD=$record
wait_for() {
for _ in {1..200}; do
for file in "$@"; do [[ ! -e $file ]] || return 0; done
sleep 0.01
done
fail "concurrent recording command reached its barrier" "$*"
}
assert_recording() {
[[ -s $XDG_RUNTIME_DIR/omarchy-screenrecord-pid && -s $XDG_RUNTIME_DIR/omarchy-screenrecord-filename ]] ||
fail "the new recording retains its saved state"
[[ -f $XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules ]] || fail "the new recording retains its audio mix"
for module in $(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules"); do
[[ -f $tmp/module-$module ]] || fail "the new recording's audio module stays loaded" "$module"
done
}
echo 123 >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pid"
printf '10\n11\n12\n' >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules"
touch "$tmp/module-10" "$tmp/module-11" "$tmp/module-12"
echo 20 >"$tmp/next-module"
TEST_ROLE=old-status "$record" --status >/dev/null 2>&1 & old=$!
wait_for "$tmp/checked"
TEST_ROLE=new-start "$record" --fullscreen --resolution=1280x800 --with-desktop-audio --with-microphone-audio >/dev/null 2>&1 & new=$!
wait_for "$tmp/lock-attempted" "$tmp/recorder-pid"
if [[ ! -e $tmp/lock-attempted ]]; then
wait_for "$XDG_RUNTIME_DIR/omarchy-screenrecord-pid"
for _ in {1..200}; do
[[ $(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-pid") == 123 ]] || break
sleep 0.01
done
[[ $(<"$XDG_RUNTIME_DIR/omarchy-screenrecord-pid") != 123 ]] || fail "the concurrent recording saves its PID"
fi
echo release >"$tmp/release"
status=0
wait "$old" || status=$?
(( status == 1 )) || fail "the old status reports its exited recorder" "$status"
wait "$new" || fail "the new recording starts"
assert_recording
pass "stale status cleanup cannot unload a concurrent new recording's audio"
rm -f "$tmp/checked" "$tmp/lock-attempted"
TEST_ROLE=old-stop "$record" --stop-recording >/dev/null 2>&1 & old=$!
wait_for "$tmp/checked"
rm "$tmp/recorder-pid"
stop_locked=0
if ! /usr/bin/flock -n "$XDG_RUNTIME_DIR/omarchy-screenrecord.lock" true; then stop_locked=1; fi
TEST_ROLE=new-start "$record" --fullscreen --resolution=1280x800 --with-desktop-audio --with-microphone-audio >/dev/null 2>&1 & new=$!
wait_for "$tmp/lock-attempted" "$tmp/recorder-pid"
if (( ! stop_locked )); then
wait "$new" || fail "the concurrent recording starts while the old stop is paused"
new=""
fi
echo release >"$tmp/release"
wait "$old" || fail "the old recording stops"
if [[ -n $new ]]; then
wait "$new" || fail "another recording starts after stop"
fi
assert_recording
pass "a finishing stop cannot remove a concurrent new recording's state"
"$record" --stop-recording >/dev/null 2>&1 || fail "the earlier recording stops"
# A recorder that never creates its file keeps the start waiting. Another
# toggle, or a stop, cancels that start instead of queueing a new one behind it.
for second in toggle stop; do
# Names are per second, so an earlier recording's file would look ready.
rm -f "$tmp/recorder-pid" "$tmp/recordings/"*
# The stop's recorder ignores TERM, so ending it needs the KILL that follows.
ignore_term=""
[[ $second == "stop" ]] && ignore_term=1
TEST_IGNORE_TERM=$ignore_term TEST_NEVER_READY=1 "$record" --fullscreen --resolution=1280x800 --with-desktop-audio --with-microphone-audio >/dev/null 2>&1 & pending=$!
wait_for "$tmp/recorder-pid"
waiting=$(<"$tmp/recorder-pid")
args=(--fullscreen --resolution=1280x800)
[[ $second == "stop" ]] && args=(--stop-recording)
timeout 5 "$record" "${args[@]}" >/dev/null 2>&1 || fail "a $second during a pending start returns at once"
timeout 5 tail --pid="$pending" -f /dev/null || fail "the pending start ends once a $second cancels it"
wait "$pending" || true
! kill -0 "$waiting" 2>/dev/null || fail "a cancelled start stops its recorder"
[[ $(<"$tmp/recorder-pid") == "$waiting" ]] || fail "a $second during a pending start does not begin another recording"
for state in pid filename pa-modules starting cancel; do
[[ ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-$state ]] || fail "a cancelled start leaves no $state behind"
done
pass "a $second during a start that is still waiting on its recorder cancels it"
done
# A stop that arrives while the start is saving its recording as started still
# ends that recording.
rm -f "$tmp/recorder-pid" "$tmp/recordings/"* "$tmp/indicator-paused"
TEST_PAUSE_INDICATOR=1 "$record" --fullscreen --resolution=1280x800 --with-desktop-audio >/dev/null 2>&1 & pending=$!
wait_for "$tmp/indicator-paused"
committed=$(<"$tmp/recorder-pid")
timeout 5 "$record" --stop-recording >/dev/null 2>&1 || fail "a stop during the start's last step returns"
echo release >"$tmp/release"
timeout 5 tail --pid="$pending" -f /dev/null || fail "the start finishes after its last step"
wait "$pending" || true
! kill -0 "$committed" 2>/dev/null || fail "a stop during the start's last step ends the recording"
for state in pid filename pa-modules starting cancel; do
[[ ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-$state ]] || fail "a stop during the start's last step leaves no $state behind"
done
pass "a stop while the start saves its recording as started still ends it"
# A stop that finds no start pending while the start is still finishing waits
# for the lock, and the recording ends either way.
rm -f "$tmp/recorder-pid" "$tmp/recordings/"* "$tmp/late-stop-sent" "$tmp/late-stop-pid"
TEST_LATE_STOP=1 "$record" --fullscreen --resolution=1280x800 >/dev/null 2>&1 || fail "the start with a late stop finishes"
started=$(<"$tmp/recorder-pid")
timeout 10 tail --pid="$(<"$tmp/late-stop-pid")" -f /dev/null || fail "the late stop finishes"
! kill -0 "$started" 2>/dev/null || fail "a stop waiting behind a finishing start still ends the recording"
[[ ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-pid && ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-cancel ]] ||
fail "a late stop leaves no state behind"
pass "a stop that waits behind a finishing start still ends the recording"
"$record" --stop-recording >/dev/null 2>&1 || true
# A start that was killed leaves its marker behind. A toggle that waits on the
# lock (held here as a stop would) does not take that stale marker for a start
# still in progress: it waits its turn and starts the recording.
rm -f "$tmp/recorder-pid" "$tmp/recordings/"* "$XDG_RUNTIME_DIR/omarchy-screenrecord-cancel"
true & dead=$!
wait "$dead"
echo "$dead" >"$XDG_RUNTIME_DIR/omarchy-screenrecord-starting"
/usr/bin/flock "$XDG_RUNTIME_DIR/omarchy-screenrecord.lock" sleep 2 & holder=$!
sleep 0.3
timeout 15 "$record" --fullscreen --resolution=1280x800 >/dev/null 2>&1 || fail "a toggle behind a stale start marker finishes"
wait "$holder" || true
[[ -s $XDG_RUNTIME_DIR/omarchy-screenrecord-pid ]] && kill -0 "$(<"$tmp/recorder-pid")" 2>/dev/null ||
fail "a toggle behind a stale start marker starts the recording instead of cancelling a start that is gone"
"$record" --stop-recording >/dev/null 2>&1 || true
pass "a killed start's leftover marker does not swallow a later toggle"
+197
View File
@@ -0,0 +1,197 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
SCRIPT="$ROOT/bin/omarchy-capture-screenrecording-process"
tmp=$(mktemp -d)
cleanup() {
local file
for file in "$tmp"/*.pid; do
[[ -f $file ]] && kill -KILL "$(<"$file")" 2>/dev/null || true
done
rm -rf "$tmp"
}
trap cleanup EXIT
mkdir -p "$tmp/bin"
# pgrep reports the pids listed in $TEST_STATE/running, as a recorder search would.
cat >"$tmp/bin/pgrep" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >"$TEST_STATE/pgrep-args"
[[ -s $TEST_STATE/running ]] || exit 1
cat "$TEST_STATE/running"
SH
chmod +x "$tmp/bin/pgrep"
export TEST_STATE="$tmp"
# A fake /proc gives each pid the executable link only Linux provides.
exe() {
mkdir -p "$tmp/proc/$1"
ln -sfn "$2" "$tmp/proc/$1/exe"
}
recorders() {
local pid
printf '%s\n' "$@" >"$tmp/running"
for pid; do
[[ -L $tmp/proc/$pid/exe ]] || exe "$pid" /usr/bin/gpu-screen-recorder
done
}
run() {
local status=0
OMARCHY_PROC_ROOT="$tmp/proc" PATH="$tmp/bin:$PATH" "$SCRIPT" "$@" 2>"$tmp/stderr" || status=$?
echo "$status"
}
# Job control keeps SIGINT deliverable to background jobs, which a
# non-interactive shell would otherwise start with it ignored.
set -m
spawn() {
local name=$1
bash -c 'trap "echo INT >\"$0\"; exit 0" INT; while :; do sleep 0.05; done' "$tmp/$name.signal" &
echo $! >"$tmp/$name.pid"
disown
}
got_int() {
local name=$1 tries=0
while [[ ! -s $tmp/$name.signal ]] && ((tries < 100)); do
sleep 0.02
tries=$((tries + 1))
done
[[ -s $tmp/$name.signal ]]
}
alive() {
kill -0 "$1" 2>/dev/null
}
wait_dead() {
local tries=0
while alive "$1" && ((tries < 100)); do
sleep 0.02
tries=$((tries + 1))
done
! alive "$1"
}
: >"$tmp/running"
[[ $(run) == 1 ]] || fail "no recorder running exits 1"
[[ $(run --pid 123) == 1 ]] || fail "a pid with no recorder running exits 1"
pass "no recorder running exits 1"
recorders 4242
[[ $(run) == 0 ]] || fail "a running recorder exits 0"
[[ $(<"$tmp/pgrep-args") == "-u $UID -x gpu-screen-reco(rder)?|wf-recorder" ]] ||
fail "recorders are found by the current user's process name" "$(<"$tmp/pgrep-args")"
pass "a running recorder exits 0"
recorders 4242 4343
[[ $(run --pid 4343) == 0 ]] || fail "--pid of a recorder exits 0"
[[ $(run --pid 04343) == 0 ]] || fail "--pid ignores leading zeros"
[[ $(run --pid 0000000000000004343) == 0 ]] || fail "--pid ignores leading zeros past ten digits"
[[ $(run --pid 4194304) == 1 ]] || fail "--pid accepts the largest Linux pid"
[[ $(run --pid 999) == 1 ]] || fail "--pid of a non-recorder exits 1"
[[ $(run --pid 434) == 1 ]] || fail "--pid matches whole pids only"
[[ $(run --pid 999 --pid 4242) == 0 ]] || fail "repeated --pid matches any of them"
pass "--pid restricts the check to the given recorder pids"
spawn a
spawn b
spawn other
recorders "$(<"$tmp/a.pid")" "$(<"$tmp/b.pid")"
[[ $(run --signal INT) == 0 ]] || fail "--signal INT with recorders exits 0"
got_int a && got_int b || fail "--signal INT interrupts every recorder"
alive "$(<"$tmp/other.pid")" && [[ ! -s $tmp/other.signal ]] || fail "--signal INT leaves other processes alone"
pass "--signal INT interrupts every recorder and nothing else"
spawn c
spawn d
recorders "$(<"$tmp/c.pid")" "$(<"$tmp/d.pid")"
[[ $(run --pid "$(<"$tmp/c.pid")" --signal INT) == 0 ]] || fail "--pid with --signal INT exits 0"
got_int c || fail "--pid with --signal INT interrupts the given recorder"
[[ ! -s $tmp/d.signal ]] && alive "$(<"$tmp/d.pid")" || fail "--pid with --signal INT leaves other recorders alone"
[[ $(run --pid "$(<"$tmp/d.pid")" --signal KILL) == 0 ]] || fail "--signal KILL exits 0"
wait_dead "$(<"$tmp/d.pid")" || fail "--signal KILL kills the given recorder"
[[ ! -s $tmp/d.signal ]] || fail "--signal KILL does not interrupt first"
alive "$(<"$tmp/other.pid")" || fail "--signal KILL leaves other processes alone"
[[ $(run --pid 999 --signal KILL) == 1 ]] || fail "--signal with no matching recorder exits 1"
alive "$(<"$tmp/other.pid")" || fail "--signal with no matching recorder signals nothing"
pass "--pid with --signal INT or KILL sends that signal to only the given recorder"
spawn e
gone=$(<"$tmp/d.pid")
recorders "$gone" "$(<"$tmp/e.pid")"
[[ $(run --signal INT) == 0 ]] || fail "a recorder exiting before its signal is not an error"
got_int e || fail "a recorder exiting before its signal does not stop the others being signalled"
pass "a recorder exiting before its signal is not an error"
exe 5001 /usr/bin/gpu-screen-recorder-helper
exe 5002 "/usr/bin/wf-recorder (deleted)"
recorders 5001
[[ $(run) == 1 ]] || fail "a process named like a recorder but running another executable is not a recorder"
[[ $(run --pid 5001) == 1 ]] || fail "--pid of a recorder-prefixed helper exits 1"
recorders 5003
rm -rf "$tmp/proc/5003"
[[ $(run) == 1 ]] || fail "a candidate whose executable cannot be read is not a recorder"
recorders 5002
[[ $(run) == 0 ]] || fail "a recorder whose executable an upgrade replaced is still a recorder"
spawn helper
exe "$(<"$tmp/helper.pid")" /usr/bin/gpu-screen-recorder-helper
recorders "$(<"$tmp/helper.pid")"
[[ $(run --signal INT) == 1 ]] || fail "--signal with only a recorder-prefixed helper exits 1"
alive "$(<"$tmp/helper.pid")" && [[ ! -s $tmp/helper.signal ]] || fail "--signal leaves a recorder-prefixed helper alone"
pass "the executable, not the truncated process name, decides what is a recorder"
true &
dead1=$!
wait "$dead1"
true &
dead2=$!
wait "$dead2"
recorders "$dead1" "$dead2"
[[ $(run --signal INT) == 1 ]] || fail "--signal exits 1 when every recorder exits before its signal"
pass "--signal exits 1 when no signal was delivered"
for args in "--pid" "--pid abc" "--pid -1" "--pid 12x" "--signal" "--signal TERM" "--signal int" "--bogus" "123" "--pid 1 extra" \
"--pid 4194305" "--pid 12345678901" "--pid 18446744073709555959" "--pid 99999999999999999999"; do
# shellcheck disable=SC2086
[[ $(run $args) == 2 ]] || fail "invalid arguments exit 2: $args"
grep -q '^Usage: omarchy-capture-screenrecording-process ' "$tmp/stderr" || fail "invalid arguments print usage: $args"
done
pass "invalid arguments print usage and exit 2"
# The real pgrep sees Linux's 15-character process names, which cut
# gpu-screen-recorder short.
if [[ -r /proc/self/comm ]]; then
mkdir -p "$tmp/real"
cp "$(command -v sleep)" "$tmp/real/gpu-screen-recorder"
cp "$(command -v sleep)" "$tmp/real/wf-recorder"
"$tmp/real/gpu-screen-recorder" 30 &
gpu=$!
echo "$gpu" >"$tmp/gpu.pid"
"$tmp/real/wf-recorder" 30 &
wf=$!
echo "$wf" >"$tmp/wf.pid"
disown -a
sleep 0.2
"$SCRIPT" --pid "$gpu" || fail "the real pgrep finds gpu-screen-recorder"
"$SCRIPT" --pid "$wf" || fail "the real pgrep finds wf-recorder"
"$SCRIPT" --pid "$gpu" --pid "$wf" --signal KILL || fail "the real pgrep signals both recorders"
wait_dead "$gpu" && wait_dead "$wf" || fail "the real recorders are killed"
pass "the real pgrep finds and signals both recorders by name"
else
skip "the real pgrep finds and signals both recorders by name (needs Linux /proc)"
fi
grep -Fq '["omarchy-capture-screenrecording", "--status"]' "$ROOT/shell/plugins/bar/indicators/ScreenRecording.qml" ||
fail "the bar recording indicator asks the recorder script, which stop also uses"
! grep -Fq 'gpu-screen-recorder' "$ROOT/shell/plugins/bar/indicators/ScreenRecording.qml" ||
fail "the bar recording indicator still greps gpu-screen-recorder"
grep -Fq '"when":"omarchy-capture-screenrecording --status"' "$ROOT/default/omarchy/omarchy-menu.jsonc" ||
fail "Stop Screenrecording asks the recorder script, which its action also uses"
! grep -Fq "pgrep -f '^gpu-screen-recorder'" "$ROOT/default/omarchy/omarchy-menu.jsonc" ||
fail "Stop Screenrecording still greps gpu-screen-recorder"
pass "the bar indicator and the menu ask the recorder script whether its stop has a recording to end"
+30
View File
@@ -0,0 +1,30 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
require_command ffmpeg
require_command ffprobe
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
# loudnorm declares 192 kHz output, so without a pinned rate the finalize pass
# writes 96 kHz AAC from a 48 kHz recording. Many editors decode such a track as
# silence, and on Apple Silicon playing it switches the speakers to 96 kHz,
# which their safety daemon answers by locking the amps.
source <(sed -n '/^finalize_recording() {/,/^}/p' "$ROOT/bin/omarchy-capture-screenrecording")
recording="$work/recording.mp4"
ffmpeg -loglevel error -f lavfi -i testsrc=size=64x64:rate=30 -f lavfi -i sine=frequency=440:sample_rate=48000 \
-t 1 -c:v libx264 -c:a aac -ar 48000 "$recording"
RECORDING_FILE="$work/latest"
echo "$recording" >"$RECORDING_FILE"
finalize_recording
# The pass trims the first 0.1 s, so a shorter file proves it ran.
duration=$(ffprobe -v error -show_entries format=duration -of csv=p=0 "$recording")
awk -v d="$duration" 'BEGIN { exit !(d < 0.95) }' || fail "the loudness pass processed the recording" "$duration"
rate=$(ffprobe -v error -select_streams a:0 -show_entries stream=sample_rate -of csv=p=0 "$recording")
[[ $rate == "48000" ]] || fail "the finalized recording keeps 48 kHz audio" "$rate"
pass "the finalized recording keeps 48 kHz audio"
+57
View File
@@ -0,0 +1,57 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
mkdir -p "$tmp/bin" "$tmp/runtime" "$tmp/recordings"
cat >"$tmp/bin/omarchy-capture-screenrecording-process" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_HELPER_CALLS"
[[ ${1:-} == "--pid" && ${2:-} == "123" && ${TEST_RECORDER_ACTIVE:-0} == "1" ]]
SH
cat >"$tmp/bin/pactl" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$TEST_AUDIO_CALLS"
SH
chmod +x "$tmp/bin/omarchy-capture-screenrecording-process" "$tmp/bin/pactl"
export PATH="$tmp/bin:$ROOT/bin:$PATH"
export XDG_RUNTIME_DIR="$tmp/runtime"
export OMARCHY_SCREENRECORD_DIR="$tmp/recordings"
export TEST_HELPER_CALLS="$tmp/helper-calls"
export TEST_AUDIO_CALLS="$tmp/audio-calls"
echo 123 >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pid"
for action in --status --stop-recording; do
printf '10\n11\n12\n' >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules"
: >"$TEST_HELPER_CALLS"
: >"$TEST_AUDIO_CALLS"
status=0
"$ROOT/bin/omarchy-capture-screenrecording" "$action" >/dev/null 2>&1 || status=$?
(( status == 1 )) || fail "$action reports no recorder after it exits" "$status"
[[ $(<"$TEST_AUDIO_CALLS") == $'unload-module 12\nunload-module 11\nunload-module 10' ]] ||
fail "$action releases the exited recorder's audio loopbacks before its sink" "$(<"$TEST_AUDIO_CALLS")"
[[ ! -e $XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules ]] || fail "$action removes the stale audio module list"
! grep -q -- '--signal' "$TEST_HELPER_CALLS" || fail "$action signals no other recorder"
pass "$action releases audio capture after the saved recorder exits"
done
printf '10\n11\n12\n' >"$XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules"
: >"$TEST_AUDIO_CALLS"
TEST_RECORDER_ACTIVE=1 "$ROOT/bin/omarchy-capture-screenrecording" --status || fail "status recognizes the active recorder"
[[ ! -s $TEST_AUDIO_CALLS && -e $XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules ]] ||
fail "status leaves an active recording's audio modules loaded"
pass "status preserves the active recorder's audio capture"
rm "$XDG_RUNTIME_DIR/omarchy-screenrecord-pid"
: >"$TEST_AUDIO_CALLS"
status=0
"$ROOT/bin/omarchy-capture-screenrecording" --status >/dev/null 2>&1 || status=$?
(( status == 1 )) || fail "status reports inactive before the recorder starts"
[[ ! -s $TEST_AUDIO_CALLS && -e $XDG_RUNTIME_DIR/omarchy-screenrecord-pa-modules ]] ||
fail "status preserves the audio mix created before the recorder starts"
pass "status leaves the starting recorder's audio mix alone"
+131 -2
View File
@@ -307,7 +307,8 @@ pass "webcam size rules place the initial window in its final corner"
recording_dir="$tmp_dir/recordings"
mkdir -p "$recording_dir"
cat >"$stub_bin/pgrep" <<'SH'
# Nothing is recording yet, whatever else runs on the machine.
cat >"$stub_bin/omarchy-capture-screenrecording-process" <<'SH'
#!/bin/bash
exit 1
SH
@@ -331,7 +332,7 @@ cat >"$stub_bin/omarchy-shell" <<'SH'
exit 0
SH
chmod +x "$stub_bin"/pgrep "$stub_bin"/omarchy-hyprland-monitor-focused \
chmod +x "$stub_bin"/omarchy-capture-screenrecording-process "$stub_bin"/omarchy-hyprland-monitor-focused \
"$stub_bin"/gpu-screen-recorder "$stub_bin"/omarchy-shell
# Compare that name across the run rather than demanding it be absent: the
@@ -410,3 +411,131 @@ pass "without a runtime dir the webcam anchors to the recorded region"
mode=$(stat -c '%a' "$state_home/omarchy" 2>/dev/null || stat -f '%Lp' "$state_home/omarchy")
[[ $mode == "700" ]] || fail "fallback directory is private even when it already existed" "mode: $mode"
pass "fallback directory is private even when it already existed"
# gpu-screen-recorder aborts before recording on a GPU it does not know, such
# as Apple Silicon's. wf-recorder takes over when it is installed, on the same
# monitor, and its pid is the one stop and status act on.
cat >"$stub_bin/gpu-screen-recorder" <<'SH'
#!/bin/bash
echo "gsr error: unknown gpu vendor" >&2
exit 1
SH
cat >"$stub_bin/wf-recorder" <<'SH'
#!/bin/bash
printf '%s\n' "$@" >"$OMARCHY_TEST_WF_ARGS"
echo "$$" >"$OMARCHY_TEST_WF_PID"
for i in "$@"; do
[[ -n ${take_next:-} ]] && { : >"$i"; break; }
[[ $i == "-f" ]] && take_next=1
done
sleep 5
SH
chmod +x "$stub_bin/gpu-screen-recorder" "$stub_bin/wf-recorder"
# Fresh directories: a recording named for the same second must not exist yet.
wf_runtime="$tmp_dir/wf-runtime"
wf_recordings="$tmp_dir/wf-recordings"
mkdir -p "$wf_runtime" "$wf_recordings"
wf_args="$tmp_dir/wf-args"
wf_pid="$tmp_dir/wf-pid"
XDG_RUNTIME_DIR="$wf_runtime" OMARCHY_TEST_WF_ARGS="$wf_args" OMARCHY_TEST_WF_PID="$wf_pid" \
OMARCHY_SCREENRECORD_DIR="$wf_recordings" \
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1
pkill -f "$stub_bin/wf-recorder" 2>/dev/null || true
[[ -s $wf_args ]] || fail "wf-recorder records when gpu-screen-recorder cannot start"
grep -Fxq -- '-o' "$wf_args" && grep -Fxq DP-1 "$wf_args" && grep -Fxq 48000 "$wf_args" ||
fail "wf-recorder records the focused monitor at 48 kHz" "$(<"$wf_args")"
[[ $(<"$wf_runtime/omarchy-screenrecord-pid") == "$(<"$wf_pid")" ]] ||
fail "the recorder pid is the wf-recorder that is recording" "$(ls -a "$wf_runtime")"
[[ $(<"$wf_runtime/omarchy-screenrecord-filename") == "$wf_recordings"/* ]] ||
fail "the wf-recorder recording is the one recorded as started"
pass "wf-recorder records when gpu-screen-recorder cannot start"
# Without wf-recorder, a recorder that cannot start leaves nothing behind.
rm "$stub_bin/wf-recorder"
none_runtime="$tmp_dir/none-runtime"
none_recordings="$tmp_dir/none-recordings"
mkdir -p "$none_runtime" "$none_recordings"
echo 424242 >"$none_runtime/omarchy-screenrecord-pid"
XDG_RUNTIME_DIR="$none_runtime" OMARCHY_SCREENRECORD_DIR="$none_recordings" \
"$ROOT/bin/omarchy-capture-screenrecording" --fullscreen >/dev/null 2>&1 || true
[[ ! -e $none_runtime/omarchy-screenrecord-pid && ! -e $none_runtime/omarchy-screenrecord-filename ]] ||
fail "a recorder that cannot start records no state, and clears a stale pid" "$(ls -a "$none_runtime")"
pass "a recorder that cannot start records no state, and clears a stale pid"
# Another recorder runs in both cases below: the helper answers for any
# selection but a pid, and a stop by name ends it.
helper_calls="$tmp_dir/helper-calls"
recording_flag="$tmp_dir/recording"
cat >"$stub_bin/omarchy-capture-screenrecording-process" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$OMARCHY_TEST_HELPER_CALLS"
if [[ $1 == "--pid" ]]; then
[[ $2 == "${OMARCHY_TEST_LIVE_PID:-}" ]]
exit
fi
if [[ $* == *"--signal INT" ]]; then
rm -f "$OMARCHY_TEST_RECORDING"
exit 0
fi
[[ -e $OMARCHY_TEST_RECORDING ]]
SH
chmod +x "$stub_bin/omarchy-capture-screenrecording-process"
# A saved pid that is no longer a recorder is a recording that ended without a
# stop: nothing of ours records, so stop has nothing to do and signals no other
# recorder the user runs.
stale_runtime="$tmp_dir/stale-runtime"
mkdir -p "$stale_runtime"
echo 424242 >"$stale_runtime/omarchy-screenrecord-pid"
touch "$recording_flag"
: >"$helper_calls"
if XDG_RUNTIME_DIR="$stale_runtime" OMARCHY_TEST_HELPER_CALLS="$helper_calls" OMARCHY_TEST_RECORDING="$recording_flag" \
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
"$ROOT/bin/omarchy-capture-screenrecording" --stop-recording >/dev/null 2>&1; then
fail "stop finds no recording of ours behind a stale pid" "$(<"$helper_calls")"
fi
! grep -q -- '--signal' "$helper_calls" ||
fail "stop signals nothing when the saved pid is stale" "$(<"$helper_calls")"
[[ -e $recording_flag ]] || fail "the other recorder keeps recording"
pass "a stale saved pid leaves every other recorder alone"
# Without a saved pid (a recording started before the pid was saved), status
# and stop select every recorder the user runs, as the bar and the menu do.
legacy_runtime="$tmp_dir/legacy-runtime"
mkdir -p "$legacy_runtime"
touch "$recording_flag"
: >"$helper_calls"
XDG_RUNTIME_DIR="$legacy_runtime" OMARCHY_TEST_HELPER_CALLS="$helper_calls" OMARCHY_TEST_RECORDING="$recording_flag" \
OMARCHY_SCREENRECORD_DIR="$recording_dir" \
"$ROOT/bin/omarchy-capture-screenrecording" --stop-recording >/dev/null 2>&1 ||
fail "stop finds a recording started before the pid was saved" "$(<"$helper_calls")"
grep -Fxq -- '--signal INT' "$helper_calls" ||
fail "stop signals the user's recorders when no pid was saved" "$(<"$helper_calls")"
! grep -Fq -- '--signal KILL' "$helper_calls" ||
fail "a recorder that stops on INT is not killed" "$(<"$helper_calls")"
pass "without a saved pid, status and stop select the user's recorders"
# The bar indicator and the menu's Stop row ask --status, so they show a stop
# exactly when the toggle has a recording of ours to end. It needs no
# recordings directory, never notifies and never signals.
status() {
XDG_RUNTIME_DIR="$1" OMARCHY_TEST_HELPER_CALLS="$helper_calls" OMARCHY_TEST_RECORDING="$recording_flag" \
OMARCHY_TEST_LIVE_PID="${2:-}" OMARCHY_SCREENRECORD_DIR="$tmp_dir/no-such-recordings" \
"$ROOT/bin/omarchy-capture-screenrecording" --status >/dev/null 2>&1
}
touch "$recording_flag"
: >"$helper_calls"
rm -f "$OMARCHY_TEST_NOTIFICATION_ARGS"
echo 424242 >"$stale_runtime/omarchy-screenrecord-pid"
if status "$stale_runtime"; then fail "--status reports nothing of ours behind a stale pid"; fi
status "$legacy_runtime" || fail "--status reports a recording started before the pid was saved"
status "$stale_runtime" 424242 || fail "--status reports the recorder its saved pid names"
rm -f "$recording_flag"
if status "$legacy_runtime"; then fail "--status reports nothing when no recorder runs"; fi
! grep -q -- '--signal' "$helper_calls" || fail "--status signals nothing" "$(<"$helper_calls")"
[[ ! -e $OMARCHY_TEST_NOTIFICATION_ARGS ]] || fail "--status never notifies, even without a recordings directory" "$(<"$OMARCHY_TEST_NOTIFICATION_ARGS")"
pass "--status answers what stop would act on, without a recordings directory, notifications or signals"
@@ -0,0 +1,110 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
# wf-recorder takes one audio device, so desktop and microphone audio together
# go through a throwaway null sink. A source whose loopback won't load is left
# out with a notification naming it, never recorded as silence.
tmp=$(mktemp -d)
trap 'rm -rf "$tmp"' EXIT
awk '
/^(wf_audio_device|wf_audio_unavailable|wf_audio_cleanup)\(\) \{/ { copying = 1 }
copying { print }
/^}/ { copying = 0 }
' "$ROOT/bin/omarchy-capture-screenrecording" >"$tmp/functions"
mkdir -p "$tmp/bin"
# Modules number from 10 up; a load fails when its arguments name a source
# listed in $tmp/refuse.
cat >"$tmp/bin/pactl" <<'SH'
#!/bin/bash
echo "pactl $*" >>"$TMP/calls"
case $1 in
get-default-sink) [[ -e $TMP/no-sink ]] || echo speakers ;;
get-default-source) [[ -e $TMP/no-source ]] || echo mic ;;
load-module)
for refused in $(cat "$TMP/refuse" 2>/dev/null); do
[[ " $* " != *" $refused "* ]] || exit 1
done
n=$(( $(cat "$TMP/next" 2>/dev/null || echo 10) ))
echo $(( n + 1 )) >"$TMP/next"
echo "$n"
;;
esac
SH
cat >"$tmp/bin/omarchy-notification-send" <<'SH'
#!/bin/bash
echo "notify $*" >>"$TMP/notes"
echo "noise on stdout"
SH
chmod +x "$tmp/bin"/*
device() {
rm -f "$tmp/calls" "$tmp/notes" "$tmp/next" "$tmp/modules"
TMP=$tmp PATH="$tmp/bin:$PATH" DESKTOP_AUDIO=$1 MICROPHONE_AUDIO=$2 PA_MODULES_FILE=$tmp/modules \
bash -c 'source "$TMP/functions"; wf_audio_device' >"$tmp/out"
}
loaded() {
grep -c 'load-module' "$tmp/calls" || true
}
unloaded() {
grep 'unload-module' "$tmp/calls" | awk '{ print $3 }' | tr '\n' ' '
}
: >"$tmp/refuse"
device true true
[[ $(cat "$tmp/out") == omarchy_screenrec.monitor ]] || fail "both sources record through the null sink" "$(cat "$tmp/out")"
[[ $(cat "$tmp/modules") == $'10\n11\n12' ]] || fail "all three module ids are kept for stop" "$(cat "$tmp/modules")"
[[ ! -e $tmp/notes ]] || fail "a full mix notifies nothing" "$(cat "$tmp/notes")"
pass "desktop and microphone audio record together through the null sink"
echo "source=mic" >"$tmp/refuse"
device true true
[[ $(cat "$tmp/out") == speakers.monitor ]] || fail "a microphone that won't mix leaves desktop audio" "$(cat "$tmp/out")"
[[ $(unloaded) == "11 10 " && ! -e $tmp/modules ]] || fail "the modules that loaded are unloaded" "$(cat "$tmp/calls")"
grep -q 'notify .*microphone audio' "$tmp/notes" && ! grep -q 'desktop' "$tmp/notes" ||
fail "the dropped microphone is named" "$(cat "$tmp/notes" 2>/dev/null)"
pass "a microphone that won't mix is dropped, named, and desktop audio records alone"
echo "source=speakers.monitor" >"$tmp/refuse"
device true true
[[ $(cat "$tmp/out") == mic ]] || fail "desktop audio that won't mix leaves the microphone" "$(cat "$tmp/out")"
[[ $(unloaded) == "11 10 " ]] || fail "the modules that loaded are unloaded" "$(cat "$tmp/calls")"
grep -q 'notify .*desktop audio' "$tmp/notes" && ! grep -q 'microphone' "$tmp/notes" ||
fail "the dropped desktop audio is named" "$(cat "$tmp/notes" 2>/dev/null)"
pass "desktop audio that won't mix is dropped, named, and the microphone records alone"
echo "source=speakers.monitor source=mic" >"$tmp/refuse"
device true true
[[ ! -s $tmp/out ]] || fail "with neither source there is no audio device" "$(cat "$tmp/out")"
[[ $(grep -c '^notify' "$tmp/notes") == 2 ]] || fail "both dropped sources are named" "$(cat "$tmp/notes")"
pass "with neither loopback the recording has no audio track rather than a silent one, and says so"
echo "module-null-sink" >"$tmp/refuse"
device true true
[[ $(cat "$tmp/out") == speakers.monitor && $(loaded) == 1 ]] || fail "without the null sink desktop audio records alone" "$(cat "$tmp/out")"
grep -q 'notify .*microphone audio' "$tmp/notes" || fail "without the null sink the microphone is named" "$(cat "$tmp/notes" 2>/dev/null)"
pass "without the null sink, desktop audio records alone and the microphone is named"
: >"$tmp/refuse"
touch "$tmp/no-source"
device true true
[[ $(cat "$tmp/out") == speakers.monitor && $(loaded) == 0 ]] || fail "without a default source desktop audio records alone" "$(cat "$tmp/out")"
grep -q 'notify .*microphone audio' "$tmp/notes" || fail "a missing default source is named" "$(cat "$tmp/notes" 2>/dev/null)"
device false true
[[ ! -s $tmp/out ]] && grep -q 'notify .*microphone audio' "$tmp/notes" || fail "a microphone-only recording without a source says so" "$(cat "$tmp/out")"
rm "$tmp/no-source"
pass "a requested source with no default device is named"
device true false
[[ $(cat "$tmp/out") == speakers.monitor && ! -e $tmp/notes ]] || fail "desktop audio alone records the sink monitor" "$(cat "$tmp/out")"
device false true
[[ $(cat "$tmp/out") == mic && ! -e $tmp/notes ]] || fail "microphone audio alone records the source" "$(cat "$tmp/out")"
device false false
[[ ! -s $tmp/out && ! -e $tmp/calls ]] || fail "no audio asks pactl nothing" "$(cat "$tmp/calls")"
pass "a single source records directly and no audio asks for nothing"
+41
View File
@@ -0,0 +1,41 @@
#!/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
check=omarchy_security_require_privileged_bash_startup
printf '#!/bin/bash -p\nsource "%s"\n%s && ( %s )\n' "$ROOT/bin/omarchy-security-functions" "$check" "$check" >"$test_tmp/entrypoint"
# Privileged mode turned on after startup: the mode is set, the proof is not.
printf 'set -p\nsource "%s"\n%s\n' "$ROOT/bin/omarchy-security-functions" "$check" >"$test_tmp/late"
chmod +x "$test_tmp/entrypoint"
"$test_tmp/entrypoint" || fail "a script started as bash -p passes the startup check, from a subshell too"
pass "a script started as bash -p passes the startup check, from a subshell too"
for arguments in "" "-p"; do
if /usr/bin/bash "$test_tmp/late" $arguments; then
fail "bash that only turned -p on later is rejected" "arguments: $arguments"
fi
done
if /usr/bin/bash "$test_tmp/entrypoint"; then
fail "a script not started with -p is rejected"
fi
pass "bash not started with -p is rejected, whatever its other arguments"
# arch-chroot runs the installer in a new PID namespace under the outer /proc,
# so the caller's own PID is not the number /proc knows it by.
in_new_pid_namespace=(unshare --user --map-root-user --fork --pid)
if "${in_new_pid_namespace[@]}" /usr/bin/true 2>/dev/null; then
"${in_new_pid_namespace[@]}" /usr/bin/bash -c "for i in {1..50}; do /usr/bin/true; done; '$test_tmp/entrypoint'; exit" </dev/null ||
fail "the startup check passes in a new PID namespace under the outer /proc"
if "${in_new_pid_namespace[@]}" /usr/bin/bash -c "for i in {1..50}; do /usr/bin/true; done; /usr/bin/bash '$test_tmp/late' -p; exit" </dev/null; then
fail "bash that only turned -p on later is rejected in a new PID namespace"
fi
pass "the startup check sees its caller in a new PID namespace, as under arch-chroot"
else
skip "a new PID namespace needs unprivileged user namespaces"
fi
+27
View File
@@ -0,0 +1,27 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
# The omarchy-settings recipe ships the same files on every architecture when
# the source has default/settings-runtime-profile, which lists the files that
# decide at runtime whether they apply. Every file it lists must exist.
profile="$ROOT/default/settings-runtime-profile"
[[ -f $profile ]] || fail "the source marks itself for the omarchy-settings recipe"
listed=$(grep -oE '(^|[[:space:]])(etc|default)/[^ ,]*' "$profile" | sed 's/^[[:space:]]*//' | sort -u)
[[ -n $listed ]] || fail "the runtime profile lists its files"
while IFS= read -r path; do
[[ -e $ROOT/${path%/} ]] || fail "the runtime profile lists an existing file: $path"
done <<<"$listed"
pass "every file the runtime profile lists exists in the source"
# The files that decide at runtime, and what they decide it from.
grep -q 'omarchy-hw-platform' "$ROOT/etc/mkinitcpio.conf.d/00-omarchy-hooks.conf" ||
fail "the HOOKS baseline asks the platform"
grep -q 'modinfo' "$ROOT/etc/mkinitcpio.conf.d/thunderbolt_module.conf" ||
fail "thunderbolt is added only where the kernel has the module"
grep -qx zram-generator "$ROOT/install/omarchy-aarch64.packages" ||
fail "aarch64 installs the zram generator the zram drop-in needs"
pass "the profile's runtime decisions are in place"
+33
View File
@@ -0,0 +1,33 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
work=$(mktemp -d)
trap 'rm -rf "$work"' EXIT
mkdir "$work/bin"
cat >"$work/bin/cat" <<'STUB'
#!/bin/bash
if [[ $1 == /sys/class/dmi/id/product_name ]]; then
[[ ${DMI_STATUS:-0} == 0 ]] || exit "$DMI_STATUS"
printf '%s\n' "${DMI_MODEL:-Unknown}"
else
/usr/bin/cat "$@"
fi
STUB
cat >"$work/bin/omarchy-pkg-add" <<'STUB'
#!/bin/bash
echo "$*" >>"$SPI_CALLS"
STUB
cat >"$work/bin/sudo" <<'STUB'
#!/bin/bash
# No host filesystem or kernel mutations in this fixture.
exit 0
STUB
chmod +x "$work/bin/"*
export SPI_CALLS="$work/calls"
for failure in 1 13; do
DMI_STATUS=$failure PATH="$work/bin:$PATH" bash -eE "$ROOT/install/hardware/apple/fix-spi-keyboard.sh"
[[ ! -f $SPI_CALLS ]] || fail 'missing/unreadable DMI must not install SPI drivers'
done
DMI_MODEL=MacBookPro13,2 PATH="$work/bin:$PATH" bash -eE "$ROOT/install/hardware/apple/fix-spi-keyboard.sh"
grep -Fxq macbook12-spi-driver-dkms "$SPI_CALLS" || fail 'matching Intel Mac retains SPI driver'
pass 'missing/unreadable DMI survives errexit and matching Intel Macs retain drivers'
+13
View File
@@ -0,0 +1,13 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
bash "$ROOT/test/shell.d/fixtures/thunderbolt/policy-test.sh"
bash "$ROOT/test/shell.d/fixtures/thunderbolt/startup-test.sh"
if [[ -n ${BOLT_TEST_SOURCE:-} ]]; then
/usr/bin/python3 "$ROOT/test/shell.d/fixtures/thunderbolt/bolt-integration.py" "$ROOT"
else
skip "real boltd integration needs BOLT_TEST_SOURCE and an UMockdev environment"
fi
+127
View File
@@ -0,0 +1,127 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$SHELL_TEST_DIR/fixtures/sudo-boundary-test.sh"
# omarchy update's boot check through the real omarchy-update-boot, dispatcher
# and platform detector, inside the sudo boundary fixture: every other update
# step is a stub that records itself, and sudo records its arguments. A fake
# boot package's update-verify records itself and exits with the status it is
# given.
require_platform_fixtures "omarchy update's boot check on platform fixtures"
copy_boundary_file bin/omarchy-update
rm "$SUDO_TEST_ROOT/bin/omarchy-update-boot"
for command in omarchy-update-boot omarchy-lifecycle-dispatch omarchy-hw-platform; do
ln -sfn "$ROOT/bin/$command" "$SUDO_TEST_ROOT/bin/$command"
done
export OMARCHY_UPDATE_LOGGED=1
tmp=$boundary_tmp/boot
mkdir -p "$tmp"
for platform in aarch64-apple aarch64 x86; do
fake_platform "$tmp/$platform" "$platform"
done
# A lifecycle root whose update-verify exits with $2.
boot_package() {
local lifecycle=$1 verify=$2
rm -rf "$lifecycle"
mkdir -p "$lifecycle/usr/lib/omarchy/mac-boot"
printf '#!/bin/bash\necho update-verify >>%q\necho "the boot files do not match" >&2\nexit %s\n' "$tmp/boot-ran" "$verify" \
>"$lifecycle/usr/lib/omarchy/mac-boot/update-verify"
chmod 755 "$lifecycle"/usr/lib/omarchy/mac-boot/*
chmod -R go-w "$lifecycle"
}
boot_package "$tmp/passing" 0
boot_package "$tmp/failing" 9
boot_package "$tmp/unverified" 1
mkdir -p "$tmp/none" "$tmp/older/usr/lib/omarchy/mac-boot" "$tmp/older/var/lib/pacman/local/omarchy-mac-boot-20260921-10"
# omarchy update -y on platform $1 with the boot package in lifecycle root $2.
# The update's own PATH is fixed, so the platform's uname goes beside the
# stubbed steps.
run_update() {
local platform=$1 lifecycle=$2
reset_boundary
rm -f "$tmp/boot-ran"
cp "$tmp/$platform/bin/uname" "$SUDO_TEST_ROOT/bin/uname"
status=0
OMARCHY_PROC_ROOT="$tmp/$platform/proc" OMARCHY_LIFECYCLE_ROOT="$lifecycle" \
"$SUDO_TEST_ROOT/bin/omarchy-update" -y >"$tmp/out" 2>"$tmp/err" || status=$?
}
# The line number of the first event in the sudo log that starts with $1.
at() {
grep -n -m 1 -F -- "$1" "$SUDO_TEST_LOG" | cut -d: -f1
}
reboot_offered() {
grep -q '^step:omarchy-update-restart --reboot-only' "$SUDO_TEST_LOG"
}
# x86 and plain aarch64: the boot check is a no-op, even with a
# failing boot-package entrypoint on disk, and nothing asks for root.
for platform in x86 aarch64; do
run_update "$platform" "$tmp/failing"
(( status == 0 )) || fail "$platform: an update reports success" "status $status: $(cat "$tmp/err")"
[[ ! -e $tmp/boot-ran ]] || fail "$platform: no boot-package entrypoint runs" "$(cat "$tmp/boot-ran")"
! grep -q 'omarchy-lifecycle-dispatch' "$SUDO_TEST_LOG" || fail "$platform: the boot check asks for no root" "$(cat "$SUDO_TEST_LOG")"
[[ ! -s $tmp/err ]] || fail "$platform: the boot check prints nothing" "$(cat "$tmp/err")"
reboot_offered || fail "$platform: the reboot is offered" "$(cat "$SUDO_TEST_LOG")"
assert_boundary_cold "$platform update"
done
pass "x86 and plain aarch64 updates are unchanged: no boot check runs, nothing asks for root and the reboot is offered"
# Apple: verify runs once the last package step, AUR, is done, cold through
# the no-update wrapper, before the reboot offer.
run_update aarch64-apple "$tmp/passing"
(( status == 0 )) || fail "apple: an update whose boot check passes reports success" "status $status: $(cat "$tmp/err")"
[[ $(cat "$tmp/boot-ran") == "update-verify" ]] || fail "apple: verify runs once" "$(cat "$tmp/boot-ran")"
aur=$(at 'step:omarchy-update-aur-pkgs') verify=$(at 'sudo -N omarchy-lifecycle-dispatch update-verify') hook=$(at 'step:omarchy-hook post-update')
[[ -n $aur && -n $verify && -n $hook ]] && (( hook < aur && aur < verify )) ||
fail "apple: verify follows AUR through the no-update wrapper" "$(cat "$SUDO_TEST_LOG")"
! grep -q '^sudo omarchy-lifecycle-dispatch update-verify' "$SUDO_TEST_LOG" || fail "apple: verification never uses a reusable timestamp" "$(cat "$SUDO_TEST_LOG")"
[[ $(sed -n "$((verify - 1))p;$((verify + 1))p" "$SUDO_TEST_LOG") == $'sudo -k\nsudo -k' ]] ||
fail "apple: verification starts cold after AUR and sudo is revoked right after it" "$(cat "$SUDO_TEST_LOG")"
reboot_offered || fail "apple: a verified update offers the reboot"
assert_boundary_cold "apple update"
pass "apple: verify runs as root through the boot package, and a verified update offers the reboot"
# omarchy-update-boot takes no operation, so a caller still asking for a
# preflight is a usage error rather than a verification.
status=0
"$SUDO_TEST_ROOT/bin/omarchy-update-boot" preflight >/dev/null 2>&1 || status=$?
(( status == 2 )) || fail "omarchy-update-boot refuses an argument" "status $status"
pass "omarchy-update-boot refuses an argument"
# A failed verification lets the update finish its remaining steps, then fails
# it without offering the reboot.
run_update aarch64-apple "$tmp/unverified"
(( status == 1 )) || fail "apple: a failed verification fails the update" "status $status: $(cat "$tmp/err")"
! reboot_offered || fail "apple: a failed verification offers no reboot"
grep -q '^step:omarchy-update-stay-awake stop' "$SUDO_TEST_LOG" ||
fail "apple: after a failed verification Stay Awake is released" "$(cat "$SUDO_TEST_LOG")"
grep -q 'the boot files do not match' "$tmp/err" && grep -q 'The update is not finished' "$tmp/err" ||
fail "apple: a failed verification says why, and that the update is not finished" "$(cat "$tmp/err")"
! grep -q 'Something went wrong during the update' "$tmp/out" "$tmp/err" || fail "apple: a failed verification is not reported as a crash"
assert_boundary_cold "apple failed verification"
pass "apple: a failed verification fails the update, explained, with no reboot offered"
# A Mac without the boot package at all predates it: the update warns that its
# boot files were not verified and finishes, asking for no root.
run_update aarch64-apple "$tmp/none"
(( status == 0 )) && reboot_offered || fail "apple without the boot package: the update finishes and offers the reboot" "status $status: $(cat "$tmp/err")"
grep -q 'update-verify on aarch64-apple needs omarchy-mac-boot' "$tmp/err" && grep -q 'The boot files were not verified' "$tmp/err" ||
fail "apple without the boot package: the update says the boot files were not verified" "$(cat "$tmp/err")"
! grep -q 'omarchy-lifecycle-dispatch' "$SUDO_TEST_LOG" || fail "apple without the boot package: nothing asks for root"
pass "apple: without the boot package the update warns that the boot files were not verified"
# A boot package from before update-verify is one package update away.
run_update aarch64-apple "$tmp/older"
(( status == 1 )) && ! reboot_offered || fail "apple: a boot package without update-verify fails the update" "status $status: $(cat "$tmp/err")"
grep -q 'which omarchy-mac-boot 20260921-10 does not provide; update omarchy-mac-boot' "$tmp/err" ||
fail "apple: a boot package without update-verify is named with its version" "$(cat "$tmp/err")"
pass "apple: a boot package without update-verify fails the update and asks for its update"
+1
View File
@@ -79,6 +79,7 @@ for command in \
omarchy-update-aur-pkgs \
omarchy-update-mise \
omarchy-update-orphan-pkgs \
omarchy-update-boot \
omarchy-hook \
omarchy-update-analyze-logs \
omarchy-shell \
+82 -1
View File
@@ -52,14 +52,32 @@ chmod +x "$stub_bin/sudo" "$stub_bin/systemd-run" "$stub_bin/pacman"
replaced="$test_tmp/replaced"
# Every case says which platform it runs on (x86 unless it sets PLATFORM), and
# which boot package entrypoints are installed (none unless it sets
# LIFECYCLE_ROOT), rather than inherit the machine running the suite.
for platform in x86 aarch64-apple; do
fake_platform "$test_tmp/$platform" "$platform"
done
mkdir -p "$test_tmp/no-boot-package"
# Root ignores both fixtures and would ask the machine's own boot package, so
# as root the cases before the platform ones get a dispatcher that registers
# nothing, and the platform ones are skipped.
if (( EUID == 0 )); then
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/omarchy-lifecycle-dispatch"
chmod +x "$stub_bin/omarchy-lifecycle-dispatch"
fi
run_update() {
local platform=${PLATFORM:-x86}
OMARCHY_REPLACED_DIR="$replaced" \
RETRY_FAILS="${RETRY_FAILS:-}" \
RETRY_INSTALLS="${RETRY_INSTALLS:-}" \
PACMAN_ATTEMPTS="$test_tmp/attempts" \
CONFLICT_REPORT="$test_tmp/report" \
OWNED_PATHS="${OWNED_PATHS:-}" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
OMARCHY_PROC_ROOT="$test_tmp/$platform/proc" \
OMARCHY_LIFECYCLE_ROOT="${LIFECYCLE_ROOT:-$test_tmp/no-boot-package}" \
PATH="$stub_bin:$test_tmp/$platform/bin:$ROOT/bin:$PATH" \
bash "$ROOT/bin/omarchy-update-system-pkgs"
}
@@ -282,3 +300,66 @@ run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
[[ $(cat "$test_tmp/attempts") == 2 ]] ||
fail "a clean upgrade runs more than one pacman transaction"
pass "a clean upgrade runs a single pacman transaction"
# A platform whose boot package owns files no package does vouches for a
# takeover before anything moves. Only a boot package's answer lets it through.
require_platform_fixtures "the platform's say over a takeover"
boot_package="$test_tmp/boot-package"
mkdir -p "$boot_package/usr/lib/omarchy/mac-boot"
cat >"$boot_package/usr/lib/omarchy/mac-boot/update-takeover" <<SH
#!/bin/bash
printf '%s\\n' "\$@" >"$test_tmp/vouched"
exit "\$(cat "$test_tmp/takeover-status")"
SH
chmod 755 "$boot_package/usr/lib/omarchy/mac-boot/update-takeover"
chmod -R go-w "$boot_package"
fresh_work
echo "ours" >"$stray"
write_report omarchy-settings "$stray"
echo 0 >"$test_tmp/takeover-status"
rm -f "$test_tmp/vouched"
PLATFORM=aarch64-apple LIFECYCLE_ROOT="$boot_package" run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
fail "a takeover the boot package vouches for goes ahead" "$(cat "$test_tmp/err")"
[[ ! -e $stray && -f $replaced$stray && $(cat "$test_tmp/vouched") == "$stray" ]] ||
fail "the boot package is asked about exactly the files that move"
pass "apple: a takeover the boot package vouches for goes ahead"
fresh_work
echo "ours" >"$stray"
write_report omarchy-settings "$stray"
echo 1 >"$test_tmp/takeover-status"
if PLATFORM=aarch64-apple LIFECYCLE_ROOT="$boot_package" run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
fail "a takeover the boot package refuses stops the upgrade"
fi
[[ -f $stray && ! -e $replaced$stray && $(cat "$test_tmp/attempts") == 1 ]] ||
fail "a refused takeover moves nothing and retries nothing"
grep -Fq "$stray" "$test_tmp/err" || fail "a refused takeover names the files it keeps" "$(cat "$test_tmp/err")"
pass "apple: a takeover the boot package refuses moves nothing"
# A Mac without its boot package, or with one too old to answer, keeps the files.
older="$test_tmp/older-boot-package"
mkdir -p "$older/usr/lib/omarchy/mac-boot" "$older/var/lib/pacman/local/omarchy-mac-boot-20260921-10"
for root in "$test_tmp/no-boot-package" "$older"; do
fresh_work
echo "ours" >"$stray"
write_report omarchy-settings "$stray"
if PLATFORM=aarch64-apple LIFECYCLE_ROOT="$root" run_update >"$test_tmp/out" 2>"$test_tmp/err"; then
fail "a Mac whose boot package can't answer keeps the files" "$root"
fi
[[ -f $stray && ! -e $replaced$stray ]] || fail "a Mac whose boot package can't answer moves nothing" "$root"
grep -Fq omarchy-mac-boot "$test_tmp/err" || fail "the refusal names the boot package" "$(cat "$test_tmp/err")"
done
pass "apple: a Mac whose boot package can't answer keeps the files"
# x86 with Mac entrypoints on disk still takes over as before, asking no one.
fresh_work
echo "ours" >"$stray"
write_report omarchy-settings "$stray"
echo 1 >"$test_tmp/takeover-status"
rm -f "$test_tmp/vouched"
LIFECYCLE_ROOT="$boot_package" run_update >"$test_tmp/out" 2>"$test_tmp/err" ||
fail "x86 takes over as before" "$(cat "$test_tmp/err")"
[[ ! -e $stray && ! -e $test_tmp/vouched ]] || fail "x86 asks no boot package"
pass "x86 takes over as before, asking no boot package"
+65 -1
View File
@@ -58,6 +58,22 @@ exit "${KEYRING_TEST_PKG_MISSING:-1}"
SH
chmod +x "$stub_bin/omarchy-pkg-missing"
# KEYRING_TEST_ARM=1: Arch Linux ARM's keyring is installed.
# KEYRING_TEST_PLATFORM: the platform (default x86).
cat >"$stub_bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ($* == "archlinuxarm-keyring" && ${KEYRING_TEST_ARM:-0} == 1) || " ${KEYRING_TEST_INSTALLED:-} " == *" $* "* ]]
SH
chmod +x "$stub_bin/omarchy-pkg-present"
cat >"$stub_bin/omarchy-hw-aarch64" <<'SH'
#!/bin/bash
[[ ${KEYRING_TEST_PLATFORM:-x86} == aarch64* ]]
SH
chmod +x "$stub_bin/omarchy-hw-aarch64"
cat >"$stub_bin/omarchy-pkg-add" <<'SH'
#!/bin/bash
@@ -66,11 +82,17 @@ exit 0
SH
chmod +x "$stub_bin/omarchy-pkg-add"
# The platform's keyrings list is read at a fixed path; the test runs a copy
# rewritten to read a fixture there.
platform_keyrings="$test_tmp/platform-keyrings"
sed "s|/usr/share/omarchy-platform/keyrings|$platform_keyrings|" "$ROOT/bin/omarchy-update-keyring" >"$test_tmp/omarchy-update-keyring"
chmod +x "$test_tmp/omarchy-update-keyring"
run_keyring() {
KEYRING_TEST_LOG="$log_file" \
KEYRING_TEST_DIR="$test_tmp" \
PATH="$stub_bin:$PATH" \
"$ROOT/bin/omarchy-update-keyring" "$@"
"$test_tmp/omarchy-update-keyring" "$@"
}
# Everything healthy: the key and package are present, the reinstall works.
@@ -86,6 +108,48 @@ grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring$' "$log_file" ||
fail "update-keyring still reinstalls archlinux-keyring" "$(cat "$log_file")"
pass "update-keyring still reinstalls archlinux-keyring"
# An aarch64 machine keeps Arch Linux ARM's keyring current alongside Arch's.
: >"$log_file"
rm -f "$test_tmp/list-calls"
for platform in aarch64-apple aarch64; do
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_ARM=1 KEYRING_TEST_PLATFORM=$platform run_keyring >"$test_tmp/arm.out"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring\tarchlinuxarm-keyring$' "$log_file" ||
fail "update-keyring also reinstalls Arch Linux ARM's keyring on $platform" "$(cat "$log_file")"
done
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_ARM=1 run_keyring >"$test_tmp/x86.out"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring$' "$log_file" ||
fail "x86 reinstalls only Arch's keyring, even with Arch Linux ARM's installed" "$(cat "$log_file")"
pass "aarch64 machines also reinstall Arch Linux ARM's keyring where it is installed, x86 never"
# The platform package's list adds the installed keyrings it names: Apple
# Silicon's [asahi-alarm], with no platform branch here.
printf '%s\n' '# Asahi' '' asahi-alarm-keyring archlinuxarm-keyring other-keyring asahi-alarm-keyring >"$platform_keyrings"
printf 'asahi-alarm-keyring' >>"$platform_keyrings"
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_ARM=1 KEYRING_TEST_PLATFORM=aarch64-apple KEYRING_TEST_INSTALLED=asahi-alarm-keyring run_keyring >"$test_tmp/platform.out"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring\tarchlinuxarm-keyring\tasahi-alarm-keyring$' "$log_file" ||
fail "update-keyring reinstalls the installed keyrings the platform names, once each" "$(cat "$log_file")"
pass "update-keyring reinstalls the installed keyrings the platform names, once each"
for listed in 'asahi-alarm-keyring;reboot' '../asahi-alarm-keyring' 'asahi-alarm' '-Syu-keyring' 'asahi-alarm-keyring extra'; do
printf '%s\n%s\n' "$listed" asahi-alarm-keyring >"$platform_keyrings"
: >"$log_file"
rm -f "$test_tmp/list-calls"
KEYRING_TEST_INSTALLED=asahi-alarm-keyring run_keyring >"$test_tmp/invalid.out" 2>&1 ||
fail "update-keyring carries on past a platform list line naming $listed" "$(cat "$test_tmp/invalid.out")"
grep -qF "skipping $listed" "$test_tmp/invalid.out" ||
fail "update-keyring warns about $listed" "$(cat "$test_tmp/invalid.out")"
grep -Eq $'^sudo\tpacman\t-Sy\t--noconfirm\tarchlinux-keyring\tasahi-alarm-keyring$' "$log_file" ||
fail "update-keyring skips $listed and still reinstalls the valid keyrings, and nothing else" "$(cat "$log_file")"
done
rm -f "$platform_keyrings"
pass "update-keyring skips a platform list line naming anything but a keyring package, with a warning"
# Key and package missing: the full populate path runs and verifies at the end.
: >"$log_file"
rm -f "$test_tmp/list-calls"
+1
View File
@@ -68,6 +68,7 @@ for command in \
omarchy-update-aur-pkgs \
omarchy-update-mise \
omarchy-update-orphan-pkgs \
omarchy-update-boot \
omarchy-hook \
omarchy-update-analyze-logs \
omarchy-shell \
+2
View File
@@ -18,6 +18,7 @@ steps=(
omarchy-snapshot
omarchy-update-stay-awake
omarchy-update-dev
omarchy-update-boot
omarchy-update-keyring
omarchy-update-system-pkgs
omarchy-migrate
@@ -79,6 +80,7 @@ expected_steps() {
omarchy-hook \
omarchy-update-mise \
omarchy-update-aur-pkgs \
omarchy-update-boot \
omarchy-update-stay-awake \
omarchy-update-restart
}
+47
View File
@@ -0,0 +1,47 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$ROOT/install/helpers/usb-authorization-policy.sh"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
identity='id 046d:c53a serial "" name "USB Receiver" hash "KCUFt1MumW4Pfs/8YXWzGQB6Hsnm8qkDqFVjfY0NIBY="'
interfaces='with-interface { 03:01:01 03:01:02 03:00:00 } with-connect-type "unknown"'
location='parent-hash "m7yTNWlczwBbYn+uRP6TRsY5AceKmAZ0Et6mgy58+/o=" via-port "3-1.1.2.1"'
device="allow $identity $location $interfaces"
portable=$(usb_authorization_portable_rule "$device")
[[ $portable == "allow $identity $interfaces"' label "omarchy-usb-authorization-v1"' ]] || fail "only device location is removed"
[[ $(usb_authorization_portable_rule "${device/#allow/block}") == "$portable" ]] || fail "blocked approvals use the same portable identity"
pass "portable USB rules retain every device identity attribute"
# A canonical escaped name cannot cause its contents to become attributes.
name='name "Receiver \" via-port \"fake\" parent-hash \"fake\" $(touch /tmp/usb-injection)"'
hostile="allow id 046d:c53a serial \"\\\\\" $name hash \"hash\" $location $interfaces"
expected="allow id 046d:c53a serial \"\\\\\" $name hash \"hash\" $interfaces"' label "omarchy-usb-authorization-v1"'
[[ $(usb_authorization_portable_rule "$hostile") == "$expected" ]] || fail "quoted attribute text must remain data"
if usb_authorization_portable_rule 'allow id 046d:c53a name "unterminated' >/dev/null; then fail "reject unmatched quote"; fi
if usb_authorization_portable_rule 'allow id 046d:c53a via-port' >/dev/null; then fail "reject missing location value"; fi
pass "USB portable parsing preserves escaped and hostile descriptor text"
# Exercise the actual root entrypoint startup boundary without root work.
sed -e "s|source /usr/bin/omarchy-security-functions|source $ROOT/bin/omarchy-security-functions|" \
-e 's|source /usr/share/omarchy/install/helpers/usb-authorization-policy.sh|exit 126|' \
"$ROOT/bin/omarchy-usb-authorization-approve" >"$scratch/entry"
chmod 755 "$scratch/entry"
printf 'set -p\n' >"$scratch/decoy"
if BASH_ENV="$scratch/decoy" /usr/bin/bash "$scratch/entry" -p; then fail "ordinary Bash with decoy -p is rejected"; fi
printf 'touch "%s"\n' "$scratch/injected" >"$scratch/startup"
if BASH_ENV="$scratch/startup" "$scratch/entry" >/dev/null 2>&1; then fail "startup fixture boundary"; fi
if /usr/bin/env 'BASH_FUNC_source%%=() { touch "$USB_TEST_INJECTED"; }' USB_TEST_INJECTED="$scratch/injected" "$scratch/entry" >/dev/null 2>&1; then fail "exported function fixture boundary"; fi
[[ ! -e $scratch/injected ]] || fail "privileged entrypoint executes caller startup code"
pass "USB privileged approval rejects unsafe Bash startup"
if ! command -v g++ >/dev/null || [[ ! -f /usr/include/usbguard/Rule.hpp ]]; then
skip "native USBGuard matcher requires g++ and USBGuard development headers"
exit 0
fi
printf '%s\n' "$device" >"$scratch/device"
printf '%s\n' "$portable" >"$scratch/policy"
g++ -std=c++17 "$ROOT/test/shell.d/fixtures/usb-authorization/match.cpp" -lusbguard -o "$scratch/match"
"$scratch/match" "$scratch/policy" "$scratch/device"
File diff suppressed because it is too large. Load diff
+257
View File
@@ -0,0 +1,257 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$ROOT/bin/omarchy-usb-authorization-boot"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
machine=12345678901234567890123456789012
export calls="$scratch/calls"
: >"$calls"
# Optional integration run uses real sbctl keys and sbattach in isolated paths.
sbctl() {
printf '%s\n' "$1" >>"$calls"
[[ ${SIGN_FAIL:-0} != 1 || $1 != sign ]] || return 1
[[ ${VERIFY_FAIL:-0} != 1 || $1 != verify ]] || return 1
if [[ -n ${USB_TEST_SBCTL_CONFIG:-} ]]; then
/usr/bin/sbctl --config "$USB_TEST_SBCTL_CONFIG" "$@"
elif [[ $1 == verify ]]; then
jq -n --arg file "${!#}" --argjson signed "${TEST_SIGNED:-1}" '[{file_name: $file, is_signed: $signed}]'
fi
}
sbattach() {
if [[ -n ${USB_TEST_SBATTACH:-} ]]; then
"$USB_TEST_SBATTACH" "$@"
fi
}
omarchy-cmd-missing() { return 1; }
limine-enroll-config() { [[ ${ENROLL_FAIL:-0} != 1 ]]; }
usb_authorization_secure_boot_enabled() { [[ ${SECURE_BOOT:-1} == 1 ]]; }
# Bootloader enrollment/rollback is exercised with real EFI binaries in its own suite.
usb_authorization_verify_bootloader() { return 0; }
fixture() {
local embedded="$1" hash digest image_base
esp="$scratch/case-$RANDOM"
history="$esp/$machine/limine_history"
manifest="$history/snapshots.json"
cache="$esp/cache.json"
config="$esp/limine.conf"
mkdir -p "$history"
printf '%s\0' "$embedded" >"$esp/cmdline"
cp /usr/lib/systemd/boot/efi/linuxx64.efi.stub "$esp/image.efi"
image_base=$(objdump -p "$esp/image.efi" | awk '$1 == "ImageBase" {print "0x" $2}')
objcopy --add-section .cmdline="$esp/cmdline" --change-section-vma .cmdline="$((image_base + 0x100000))" "$esp/image.efi"
if [[ -n ${USB_TEST_SBCTL_CONFIG:-} ]]; then
sbctl sign "$esp/image.efi" >/dev/null
fi
digest=$(sha256sum "$esp/image.efi"); digest=${digest%% *}
old_name="test.efi_sha256_$digest"
cp "$esp/image.efi" "$history/$old_name"
hash=$(b2sum "$esp/image.efi"); hash=${hash%% *}
printf '/Omarchy\ncomment: machine-id=%s\n //snapshot\n comment: kernel-id=linux\n path: boot():/%s/limine_history/%s#%s\n cmdline: %s rootflags=subvol=/snapshot\n' \
"$machine" "$machine" "$old_name" "$hash" "$embedded" >"$config"
jq -n --arg name "$old_name" --arg hash "$hash" --arg machine "$machine" --arg cmdline "$embedded" '
def kernel:
{imageDetails: [{fileHashName: $name, fileName: "test.efi",
snapshotFilePathLine: "/" + $machine + "/limine_history/" + $name + "#" + $hash,
filePathLine: "/EFI/Linux/test.efi#" + $hash, properties: {HASH: "#" + $hash}}],
cmdlineDetails: [{cmdline: $cmdline, snapshotCmdline: $cmdline + " rootflags=subvol=/snapshot"}],
allInConfig: ["path: boot():/EFI/Linux/test.efi#" + $hash, "cmdline: " + $cmdline],
allInSnapshotConfig: ["comment: kernel-id=linux", "path: boot():/" + $machine + "/limine_history/" + $name + "#" + $hash, "cmdline: " + $cmdline + " rootflags=subvol=/snapshot"], subKernels: []};
{jsonFormatVersion: "1.3.0", snapshotEntries: [{kernelEntries: [kernel | .subKernels = [kernel]]}]}
' >"$manifest"
cp "$manifest" "$manifest.old"
cp "$manifest" "$cache"
cp "$manifest" "$esp/before.json"
cp "$config" "$esp/before.conf"
: >"$calls"
}
for expected in enabled disabled; do
old='quiet'
[[ $expected != disabled ]] || old+=' usbcore.authorized_default=0'
fixture "$old"
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine" "$cache"
new_name=$(jq -r '.snapshotEntries[0].kernelEntries[0].imageDetails[0].fileHashName' "$manifest")
[[ $new_name != "$old_name" && -f $history/$new_name ]] || fail "conflicting archives get new content-addressed copies"
cmp "$history/$old_name" "$esp/image.efi" || fail "original archived image remains unchanged"
digest=$(sha256sum "$history/$new_name")
[[ $new_name == *"${digest%% *}" ]] || fail "replacement name matches its content hash"
objcopy -O binary --only-section=.cmdline "$history/$new_name" "$esp/section"
embedded=$(tr '\0' '\n' <"$esp/section")
wanted=quiet
[[ $expected != enabled ]] || wanted+=' usbcore.authorized_default=0'
[[ $embedded == "$wanted" ]] || fail "replacement must preserve embedded boot arguments and change only USB policy"
objcopy -O binary --only-section=.text "$history/$old_name" "$esp/text.before"
objcopy -O binary --only-section=.text "$history/$new_name" "$esp/text.after"
cmp "$esp/text.before" "$esp/text.after" || fail "archive conversion must not alter executable code"
[[ $(grep -c '^sign$' "$calls") == 1 && $(grep -c '^verify$' "$calls") == 2 ]] || fail "deduplicated image must be verified before and after signing"
for file in "$manifest" "$manifest.old" "$cache"; do
! grep -Fq "$old_name" "$file" || fail "no old image references may survive in manifests"
usb_authorization_rewrite_snapshot_manifest "$expected" "$file"
# Both cached and structured restore representations must regenerate valid entries.
for format in cached structured; do
{
printf '/Omarchy\ncomment: machine-id=%s\n //snapshot\n' "$machine"
if [[ $format == cached ]]; then
jq -r '.snapshotEntries[0].kernelEntries[0].allInSnapshotConfig[]' "$file"
else
printf 'comment: kernel-id=linux\n'
jq -r '.snapshotEntries[0].kernelEntries[0].subKernels[0] | "path: boot():" + .imageDetails[0].snapshotFilePathLine, "cmdline: " + .cmdlineDetails[0].snapshotCmdline' "$file"
fi
} >"$esp/regenerated.conf"
usb_authorization_verify_boot_entries "$expected" "$esp/regenerated.conf" "$esp" "$machine" enabled || fail "regenerated $format $expected snapshot must verify"
done
done
usb_authorization_rewrite_boot_cmdlines "$expected" "$config" "$machine"
usb_authorization_verify_boot_entries "$expected" "$config" "$esp" "$machine" enabled || fail "$expected archive transition must verify under Secure Boot"
usb_authorization_reconcile_archived_images "$expected" "$config" "$esp" "$machine" "$cache"
[[ $(grep -c '^sign$' "$calls") == 1 ]] || fail "retry must not resign an already-compatible archive"
if [[ -n ${USB_TEST_SBCTL_CONFIG:-} ]]; then
usb_authorization_verify_signature "$history/$new_name"
fi
pass "USB $expected transition updates signed history and all cached/structured references"
done
fixture quiet
old_hash=$(b2sum "$esp/image.efi"); old_hash=${old_hash%% *}
printf ' //current\n comment: kernel-id=current\n path: boot():/image.efi#%s\n cmdline: quiet\n' "$old_hash" >>"$config"
usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache"
grep -Fq "path: boot():/image.efi#$old_hash" "$config" || fail "preflight must not rewrite hashes of unchanged current images sharing archive content"
pass "USB archive preflight preserves unchanged current-image hashes"
fixture 'quiet rd.test="two spaces"'
usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache"
new_name=$(jq -r '.snapshotEntries[0].kernelEntries[0].imageDetails[0].fileHashName' "$manifest")
objcopy -O binary --only-section=.cmdline "$history/$new_name" "$esp/section"
[[ $(tr '\0' '\n' <"$esp/section") == 'quiet rd.test="two spaces" usbcore.authorized_default=0' ]] || fail "unrelated quoted arguments must retain their whitespace"
pass "USB archive policy changes preserve unrelated embedded arguments and executable code"
for failure in SIGN_FAIL VERIFY_FAIL ENROLL_FAIL; do
fixture quiet
if (export "$failure=1"; usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache") >"$esp/failure.log" 2>&1; then
fail "$failure must prevent publication"
fi
cmp "$config" "$esp/before.conf" || fail "$failure must preserve the original boot menu"
for file in "$manifest" "$manifest.old" "$cache"; do
cmp "$file" "$esp/before.json" || fail "$failure must preserve every manifest"
done
cmp "$history/$old_name" "$esp/image.efi" || fail "$failure must preserve the archived image"
done
pass "USB archive signing, verification and enrollment failures preserve original references"
fixture quiet
printf corrupt >>"$history/$old_name"
if usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache" >"$esp/hash-failure" 2>&1; then
fail "corrupt original archive must never be signed"
fi
[[ ! -s $calls ]] || fail "hash failure must occur before signing"
cmp "$config" "$esp/before.conf"
SECURE_BOOT=0 usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache"
[[ ! -s $calls ]] || fail "non-Secure-Boot transitions must not rewrite archive images"
pass "USB archive conversion rejects corruption and skips non-Secure-Boot systems"
if [[ -z ${USB_TEST_SBCTL_CONFIG:-} ]]; then
fixture quiet
if TEST_SIGNED=0 usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache" >"$esp/unsigned" 2>&1; then
fail "a false-success sbctl result cannot authorize signing"
fi
! grep -qx sign "$calls" || fail "an unsigned original must not be resigned"
fi
for missing in menu manifest; do
fixture quiet
if [[ $missing == menu ]]; then
sed -i '/cmdline:/d' "$config"
else
jq '.snapshotEntries[0].kernelEntries[0].cmdlineDetails = []' "$manifest" >"$esp/modified"
mv "$esp/modified" "$manifest"
fi
usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache" || fail "embedded-only $missing must support policy changes"
usb_authorization_rewrite_boot_cmdlines enabled "$config" "$machine"
usb_authorization_verify_boot_entries enabled "$config" "$esp" "$machine" enabled || fail "embedded-only $missing must retain a valid command line"
done
for section in .profile .pcrsig; do
fixture quiet
printf protected >"$esp/protected"
objcopy --add-section "$section=$esp/protected" "$history/$old_name" 2>/dev/null
if usb_authorization_reconcile_archived_images enabled "$config" "$esp" "$machine" "$cache" >"$esp/protected-failure" 2>&1; then
fail "a $section image must require its original builder"
fi
grep -Fq "($section)" "$esp/protected-failure" || fail "unsupported signing policies need an actionable error"
[[ ! -s $calls ]] || fail "unsupported signing policies must not be resigned"
done
pass "USB archive conversion requires authentic signatures and preserves boot/unlock requirements"
# Package hooks use this same lock. Installing the optional signature tool
# must finish before preparation takes the lock, while sync must run under it.
fixture quiet
prepare=$(declare -f usb_authorization_prepare_archived_images)
prepare=${prepare//\/run\/lock\/boot-partition.lock/$scratch\/preflight.lock}
eval "$prepare"
omarchy-cmd-missing() { return 0; }
omarchy-pkg-add() {
flock --nonblock "$scratch/preflight.lock" true || fail "package installation must precede the boot lock"
printf 'package\n' >>"$calls"
}
limine-snapper-sync() {
[[ $* == '--no-mutex --no-hooks' ]] || fail "preparation must use the already-held lock"
if flock --nonblock "$scratch/preflight.lock" true; then
fail "snapshot synchronization must hold the boot lock"
fi
}
usb_authorization_prepare_archived_images enabled "$config" "$esp" "$machine"
grep -qx package "$calls" || fail "missing signature tooling must be installed before preparing archives"
pass "USB archive preparation installs optional tools before taking Limine's lock"
# The disposable Lab runs the actual root-only main function with only boot paths
# and external build/sync commands redirected into this fixture.
if [[ ${USB_TEST_MAIN:-0} == 1 ]]; then
(( EUID == 0 )) || fail "main-path fixtures require the disposable guest's root"
usb_authorization_prepare_archived_images() {
usb_authorization_reconcile_archived_images "$1" "$2" "$3" "$machine" "$cache"
}
limine-mkinitcpio() { return 0; }
usb_authorization_sync_and_verify() {
local file
for file in "$manifest" "$manifest.old" "$cache"; do
usb_authorization_rewrite_snapshot_manifest "$1" "$file" || return 1
done
usb_authorization_rewrite_boot_cmdlines "$1" "$2" "$machine" || return 1
usb_authorization_verify_boot_entries "$1" "$2" "$3" "$machine" enabled
}
for action in enable disable; do
old=quiet
[[ $action != disable ]] || old+=' usbcore.authorized_default=0'
fixture "$old"
drop_in="$esp/entry-tool/usb-authorization.conf"
limine_defaults="$esp/defaults"
limine_conf="$config"
boot_path="$esp"
printf '# fixture defaults\n' >"$limine_defaults"
if [[ $action == disable ]]; then
mkdir -p "${drop_in%/*}"
printf '%s\n' "$setting" >"$drop_in"
usb_authorization_enable_snapshot_setting "$limine_defaults"
fi
cp "$limine_defaults" "$esp/defaults.before"
if (export SIGN_FAIL=1; usb_authorization_main "$action") >"$esp/main-failed" 2>&1; then
fail "$action must stop if archive preparation fails"
fi
cmp "$limine_defaults" "$esp/defaults.before" || fail "$action failure must precede snapshot-setting changes"
if [[ $action == enable ]]; then
[[ ! -e $drop_in ]] || fail "enable preflight failure must not create the boot setting"
else
[[ $(<"$drop_in") == "$setting" ]] || fail "disable preflight failure must preserve the boot setting"
fi
(usb_authorization_main "$action")
if [[ $action == enable ]]; then
[[ $(<"$drop_in") == "$setting" ]] || fail "successful enable sets the boot policy"
else
[[ ! -e $drop_in ]] || fail "successful disable clears the boot policy"
fi
pass "USB $action main path prepares archives before changing boot policy"
done
fi
+183
View File
@@ -0,0 +1,183 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
source "$ROOT/bin/omarchy-usb-authorization-boot"
scratch=$(mktemp -d)
trap 'rm -rf "$scratch"' EXIT
usb_authorization_secure_boot_enabled() { return 0; }
usb_authorization_limine_settings() { printf 'x64 %s\n' "${ENROLLMENT:-yes}"; }
# Real PE enrollment always runs. An optional isolated sbctl configuration also
# exercises real signatures; default runs track signed content without host keys.
sbctl() {
if [[ $1 == sign && ${SIGN_FAIL:-0} == 1 ]]; then
return 1
fi
if [[ -n ${USB_TEST_SBCTL_CONFIG:-} ]]; then
/usr/bin/sbctl --config "$USB_TEST_SBCTL_CONFIG" "$@"
elif [[ $1 == sign ]]; then
sha256sum "${!#}" | cut -d ' ' -f1 >>"$scratch/signed"
elif [[ $1 == verify ]]; then
local digest signed=0
digest=$(sha256sum "${!#}")
if grep -qx "${digest%% *}" "$scratch/signed"; then signed=1; fi
jq -n --arg file "${!#}" --argjson signed "$signed" '[{file_name: $file, is_signed: $signed}]'
fi
}
fixture() {
case_dir=$(mktemp -d "$scratch/case.XXXXXXXX")
boot_path="$case_dir/boot"
cache="$case_dir/cache"
limine_conf="$boot_path/limine.conf"
limine_defaults="$case_dir/defaults"
drop_in="$case_dir/dropins/usb-authorization.conf"
image="$boot_path/EFI/limine/limine_x64.efi"
mkdir -p "${image%/*}" "$cache/lss" "${drop_in%/*}"
printf 'initial menu\n' >"$limine_conf"
printf 'initial kernel\n' >"$boot_path/kernel.efi"
printf 'initial snapshot\n' >"$cache/lss/snapshots.json"
printf 'initial defaults\n' >"$limine_defaults"
: >"$scratch/signed"
cp /usr/share/limine/BOOTX64.EFI "$image"
if [[ ${ENROLLMENT:-yes} == yes ]]; then
digest=$(b2sum "$limine_conf")
limine enroll-config "$image" "${digest%% *}" --quiet
fi
sbctl sign "$image" >/dev/null
cp "$image" "$case_dir/original.efi"
usb_authorization_verify_bootloader "$limine_conf" "$boot_path"
}
# Matches the installed wrapper: restore unsigned binary, enroll, attempt sign,
# then mask failure with a successful final sync. No real ESP is ever touched.
limine-enroll-config() {
cp /usr/share/limine/BOOTX64.EFI "$image"
if [[ ${ENROLLMENT:-yes} == yes && ${SKIP_ENROLLMENT:-0} != 1 ]]; then
local digest
digest=$(b2sum "$limine_conf")
limine enroll-config "$image" "${digest%% *}" --quiet || return 1
fi
sbctl sign "$image" || true
sync -f "$image"
}
# Relocate the outer real flock into scratch. The runner stands in for the
# namespace child and deliberately mutates the same files as rebuild hooks.
transaction=$(declare -f usb_authorization_boot_transaction)
transaction=${transaction//\/run\/lock\/boot-partition.lock/$scratch\/boot.lock}
eval "$transaction"
usb_authorization_run_transaction() {
if flock --nonblock "$scratch/boot.lock" true; then
fail "the real boot lock must cover the entire transaction"
fi
printf 'changed menu\n' >"$limine_conf"
printf 'changed kernel\n' >"$boot_path/kernel.efi"
printf 'changed metadata\n' >"$cache/lss/snapshots.json"
printf 'new cached backup\n' >"$cache/lss/snapshots.json.old"
mkdir -p "$boot_path/$(</etc/machine-id)/limine_history"
printf 'new archived backup\n' >"$boot_path/$(</etc/machine-id)/limine_history/snapshots.json.old"
printf 'changed defaults\n' >"$limine_defaults"
printf '%s\n' "$setting" >"$drop_in"
if [[ ${EARLY_FAILURE:-0} == 1 ]]; then
cp /usr/share/limine/BOOTX64.EFI "$image"
return 1
fi
if [[ ${UNCHECKED_HOOK:-0} == 1 ]]; then
limine-enroll-config
else
usb_authorization_enroll_config "$limine_conf" "$boot_path"
fi
}
for failure in SIGN_FAIL SKIP_ENROLLMENT EARLY_FAILURE; do
fixture
if (export "$failure=1"; usb_authorization_boot_transaction enable "$cache") >"$case_dir/failure.log" 2>&1; then
fail "$failure must reject the boot transition"
fi
cmp "$image" "$case_dir/original.efi" || fail "$failure must restore the signed bootloader byte-for-byte without signing"
[[ $(<"$limine_conf") == 'initial menu' ]] || fail "$failure must restore matching menu"
[[ $(<"$boot_path/kernel.efi") == 'initial kernel' ]] || fail "$failure must restore images referenced by that menu"
[[ $(<"$cache/lss/snapshots.json") == 'initial snapshot' ]] || fail "$failure must restore cached snapshot metadata"
[[ ! -e $cache/lss/snapshots.json.old && ! -e $boot_path/$(</etc/machine-id)/limine_history/snapshots.json.old ]] || fail "$failure must remove newly created recovery metadata"
[[ $(<"$limine_defaults") == 'initial defaults' && ! -e $drop_in ]] || fail "$failure must restore settings including an absent drop-in"
usb_authorization_verify_bootloader "$limine_conf" "$boot_path"
done
pass "USB boot transitions restore signed Limine, matching menu, images, metadata and settings on failure"
fixture
printf 'original opt-in\n' >"$drop_in"
if (SIGN_FAIL=1 UNCHECKED_HOOK=1 usb_authorization_boot_transaction disable "$cache") >"$case_dir/failure.log" 2>&1; then
fail "final verification must reject false-success rebuild hooks"
fi
cmp "$image" "$case_dir/original.efi" || fail "false-success hooks must restore original signed Limine"
[[ $(<"$drop_in") == 'original opt-in' ]] || fail "failed disable must preserve an existing drop-in"
pass "USB boot transitions independently verify enrollment after external hooks"
for enrollment in yes no; do
export ENROLLMENT="$enrollment"
fixture
usb_authorization_boot_transaction enable "$cache"
[[ $(<"$limine_conf") == 'changed menu' && -e $drop_in ]] || fail "successful transition must commit"
usb_authorization_verify_bootloader "$limine_conf" "$boot_path"
done
pass "USB boot transitions commit verified signing with enrollment enabled or disabled"
# Disposable Lab only: exercise the actual namespace runner and both lock
# inodes, with the installed enrollment/restore functions and real signatures.
if [[ ${USB_TEST_NAMESPACE:-0} == 1 ]]; then
(( EUID == 0 )) || fail "namespace tests require root in the disposable guest"
[[ -n ${USB_TEST_SBCTL_CONFIG:-} ]] || fail "namespace tests require isolated signing keys"
export ENROLLMENT=yes
for outcome in failure success; do
fixture
cp /usr/share/limine/BOOTX64.EFI "$case_dir/limine_x64.efi"
tar -cf "$case_dir/limine.bak" -C "$case_dir" limine_x64.efi
lock_inode=$(stat -Lc '%d:%i' /run/lock/boot-partition.lock)
# Source the shipped functions in both the parent and namespace child.
sed '/^if \[\[ ${BASH_SOURCE\[0\]} == "\$0" \]\]; then/,$d' "$ROOT/bin/omarchy-usb-authorization-boot" >"$case_dir/helper"
for variable in scratch case_dir boot_path cache limine_conf limine_defaults drop_in image lock_inode; do
printf '%s=%q\n' "$variable" "${!variable}" >>"$case_dir/helper"
done
declare -f sbctl usb_authorization_limine_settings usb_authorization_secure_boot_enabled >>"$case_dir/helper"
cat >>"$case_dir/helper" <<'SCRIPT'
usb_authorization_main() {
[[ $(stat -Lc '%d:%i' /run/lock/boot-partition.lock) != "$lock_inode" ]] || return 1
exec 9>/run/lock/boot-partition.lock
flock --nonblock 9 || return 1
printf 'changed menu\n' >"$limine_conf"
printf 'changed kernel\n' >"$boot_path/kernel.efi"
source /usr/lib/limine/limine-common-functions
LIMINE_DIR_PATH="${image%/*}"
LIMINE_EFI_FILE=limine_x64.efi
BINARY_TARGET_PATH="$image"
BINARY_BACKUP_PATH="$case_dir/limine.bak"
LIMINE_CONFIG_PATH="$limine_conf"
ENABLE_ENROLL_LIMINE_CONFIG=yes
is_sb_installed() { return 0; }
is_uefi() { return 0; }
limine-enroll-config() { enroll_config; }
usb_authorization_enroll_config "$limine_conf" "$boot_path"
}
if [[ ${BASH_SOURCE[0]} == "$0" ]]; then
usb_authorization_boot_transaction "$1" "$cache"
fi
SCRIPT
if [[ $outcome == failure ]]; then
if SIGN_FAIL=1 bash "$case_dir/helper" enable >"$case_dir/namespace.log" 2>&1; then
fail "actual namespace must reject installed wrapper's false success"
fi
grep -Fq 'Failed to sign' "$case_dir/namespace.log" || fail "failure must reach the installed enrollment function"
cmp "$image" "$case_dir/original.efi" || fail "namespace rollback restores original signed bootloader"
[[ $(<"$limine_conf") == 'initial menu' && $(<"$boot_path/kernel.efi") == 'initial kernel' ]] || fail "namespace rollback restores matching boot files"
else
bash "$case_dir/helper" disable >"$case_dir/namespace.log" 2>&1
[[ $(<"$limine_conf") == 'changed menu' ]] || fail "namespace success commits"
fi
usb_authorization_verify_bootloader "$limine_conf" "$boot_path"
flock --nonblock /run/lock/boot-partition.lock true || fail "outer lock must be released"
done
pass "USB real namespace isolates nested locks and recovers installed Limine signing failure"
fi
+108
View File
@@ -0,0 +1,108 @@
#!/bin/bash
set -euo pipefail
source "$(dirname "$0")/base-test.sh"
# Exercise reset staging in a temporary root with the real offline systemctl.
# Never invoke the reset command's main function or any actual disk operations.
python - "$ROOT" <<'PY'
from pathlib import Path
import os
import re
import shutil
import subprocess
import sys
import tempfile
repo = Path(sys.argv[1])
source = (repo / 'bin/omarchy-system-factory-reset').read_text()
functions = []
for name in ['install_provisioning_units', 'scrub_factory_accounts', 'sanitize_factory_baseline', 'stage_full_reset']:
match = re.search(r'^' + name + r'\(\) \{\n.*?^\}', source, re.M | re.S)
assert match, name
functions.append(match.group())
with tempfile.TemporaryDirectory(prefix='omarchy-usb-reset-') as tmp:
scratch = Path(tmp)
for scenario in ['enabled', 'disabled', 'absent', 'disable-failure']:
root = scratch / scenario
top = root / 'top'
factory = top / '@factory'
for rel in ['etc/systemd/system/basic.target.wants', 'usr/lib/systemd/system',
'etc/usbguard', 'usr/bin', 'usr/share/omarchy/install/provisioning']:
(factory / rel).mkdir(parents=True, exist_ok=True)
(top / '@').mkdir()
(top / '@' / 'original-root').touch()
(factory / 'etc/passwd').write_text('root:x:0:0:root:/root:/bin/bash\n')
(factory / 'etc/machine-id').write_text('old-machine-id\n')
policy = 'allow id 1234:0001 name "Original owner keyboard" hash "old-owner"\n'
(factory / 'etc/usbguard/rules.conf').write_text(policy)
unit = factory / 'usr/lib/systemd/system/usbguard.service'
enabled_path = Path('etc/systemd/system/basic.target.wants/usbguard.service')
if scenario != 'absent':
unit.write_text('[Unit]\nDescription=Fixture USBGuard\n[Service]\nExecStart=/bin/true\n[Install]\nWantedBy=basic.target\n')
if scenario in ['enabled', 'disable-failure']:
(factory / enabled_path).symlink_to('/usr/lib/systemd/system/usbguard.service')
owner = factory / 'usr/bin/omarchy-provision-owner'
owner.touch()
owner.chmod(0o755)
for name in ['omarchy-provision-owner.service', 'omarchy-system-factory-reset-finish.service']:
shutil.copyfile(repo / 'install/provisioning' / name,
factory / 'usr/share/omarchy/install/provisioning' / name)
harness = '''#!/bin/bash
set -euo pipefail
TOP_MNT="$1/top"
NEXT_NAME=@next
PROVISIONING_DIR=/var/lib/omarchy/provisioning
LOG_FILE="$1/log"
log() { :; }
fail() { echo "$*" >&2; exit 1; }
btrfs() {
if [[ $1 == subvolume && $2 == snapshot ]]; then
cp -a "$3" "$4"
elif [[ $1 == property ]]; then
:
else
return 90
fi
}
# Account cleanup has its own real-tool suite; keep this fixture focused on USB.
usermod() {
[[ $1 == --root && ( $2 == "$TOP_MNT/$NEXT_NAME" || $2 == "$TOP_MNT/@factory" ) &&
$3 == --password && $4 == '!' && $5 == root ]]
}
systemd-id128() { echo new-machine-id; }
encrypted_install() { return 1; }
rebuild_next_boot() { :; }
sync() { :; }
systemctl() {
# All service changes must address the staged root, never the live machine.
[[ $1 == "--root=$TOP_MNT/$NEXT_NAME" && $2 == disable && $3 == usbguard.service ]] || return 91
[[ $2 != "${FAIL_OPERATION:-}" ]] || return 92
command systemctl "$@"
}
''' + '\n\n'.join(functions) + '\nstage_full_reset\n'
runner = root / 'run'
runner.write_text(harness)
env = os.environ.copy()
env['FAIL_OPERATION'] = 'disable' if scenario == 'disable-failure' else ''
result = subprocess.run(['bash', str(runner), str(root)], env=env, capture_output=True, text=True)
if scenario == 'disable-failure':
assert result.returncode != 0, 'failed disable must abort reset staging'
assert (top / '@' / 'original-root').exists(), 'failure must not activate the reset root'
assert 'could not disable USBGuard' in result.stderr
else:
assert result.returncode == 0, result.stderr
staged = top / '@'
assert not (staged / enabled_path).is_symlink(), 'reset must not start USBGuard before enrollment'
assert (staged / 'var/lib/omarchy/provisioning/pending').exists()
assert (staged / 'etc/usbguard/rules.conf').read_text() == policy
if scenario == 'enabled':
assert (factory / enabled_path).is_symlink(), 'only the staged clone is disabled'
# The owner-enrollment helper enables the daemon after policy and
# ACL setup. Confirm an ordinary enable can reverse this disable.
subprocess.run(['systemctl', '--root=' + str(staged), 'enable', 'usbguard.service'],
check=True, capture_output=True, text=True)
assert (staged / enabled_path).is_symlink()
print('ok - factory USB enrollment staging: ' + scenario)
PY
+74
View File
@@ -0,0 +1,74 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
stub_bin="$test_tmp/bin"
lib_path="$test_tmp/lib/voxtype"
calls="$test_tmp/calls.log"
mkdir -p "$stub_bin" "$lib_path"
for command in gum omarchy-pkg-drop omarchy-pkg-add hyprctl omarchy-restart-shell omarchy-notification-send systemctl; do
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/$command"
done
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/omarchy-pkg-present"
# Voxtype is already installed, so the install asks nothing and only sets it up.
printf '#!/bin/bash\nexit 1\n' >"$stub_bin/omarchy-cmd-missing"
printf '#!/bin/bash\nexit 0\n' >"$stub_bin/omarchy-dictation-use"
cat >"$stub_bin/omarchy-hw-vulkan" <<'SH'
#!/bin/bash
(( ${VULKAN:-1} == 1 ))
SH
cat >"$stub_bin/voxtype" <<'SH'
#!/bin/bash
printf 'voxtype %s\n' "$*" >>"$TEST_LOG"
SH
chmod +x "$stub_bin"/*
run_install() {
local vulkan=$1 backend=$2
: >"$calls"
rm -rf "$test_tmp/home" "$lib_path/voxtype-vulkan"
mkdir -p "$test_tmp/home"
case $backend in
executable)
printf '#!/bin/bash\n' >"$lib_path/voxtype-vulkan"
chmod +x "$lib_path/voxtype-vulkan"
;;
unusable)
: >"$lib_path/voxtype-vulkan"
;;
esac
HOME="$test_tmp/home" OMARCHY_PATH="$ROOT" OMARCHY_VOXTYPE_LIB_PATH="$lib_path" \
VULKAN="$vulkan" TEST_LOG="$calls" PATH="$stub_bin:$PATH" \
XDG_CONFIG_HOME="$test_tmp/home/.config" bash "$ROOT/bin/omarchy-install-dictation-voxtype" >/dev/null ||
fail "Voxtype install completes (vulkan=$vulkan backend=$backend)"
grep -Fx 'voxtype setup systemd' "$calls" >/dev/null ||
fail "Voxtype install sets up the user service (vulkan=$vulkan backend=$backend)"
}
run_install 1 executable
grep -Fx 'voxtype setup gpu --enable' "$calls" >/dev/null ||
fail "Voxtype enables the GPU backend when Vulkan and the backend binary are present"
pass "Voxtype enables the GPU backend when Vulkan and the backend binary are present"
run_install 1 missing
! grep -q 'setup gpu' "$calls" ||
fail "Voxtype does not enable a Vulkan backend the package did not ship"
pass "Voxtype does not enable a Vulkan backend the package did not ship"
run_install 1 unusable
! grep -q 'setup gpu' "$calls" ||
fail "Voxtype does not enable a Vulkan backend that is not executable"
pass "Voxtype does not enable a Vulkan backend that is not executable"
run_install 0 executable
! grep -q 'setup gpu' "$calls" ||
fail "Voxtype does not enable the GPU backend without a Vulkan runtime"
pass "Voxtype does not enable the GPU backend without a Vulkan runtime"
@@ -0,0 +1,93 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
test_dir=$(mktemp -d)
trap 'rm -rf "$test_dir"' EXIT
home="$test_dir/home"
migration="$ROOT/migrations/1791470456.sh"
mkdir -p "$test_dir/bin"
call_log="$test_dir/shell-calls"
cat >"$test_dir/bin/omarchy-shell" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >>"$CALL_LOG"
SH
chmod +x "$test_dir/bin/omarchy-shell"
reset_home() {
rm -rf "$home"
mkdir -p "$home/.local/state/omarchy/current/theme/backgrounds" "$home/.local/state/omarchy/theme-backgrounds"
: >"$call_log"
}
run_migration() {
HOME="$home" OMARCHY_PATH="$ROOT" CALL_LOG="$call_log" PATH="$test_dir/bin:$PATH" \
bash -euo pipefail "$migration" >"$test_dir/output"
}
backgrounds="$home/.local/state/omarchy/current/theme/backgrounds"
current="$home/.local/state/omarchy/current/background"
remembered="$home/.local/state/omarchy/theme-backgrounds/tokyo-night"
remembered_png="$home/.local/state/omarchy/theme-backgrounds/catppuccin"
reset_home
touch "$backgrounds/current.webp"
ln -s "$backgrounds/current.jpg" "$current"
printf '%s\n' "$backgrounds/2-swirl-buck.jpg" >"$remembered"
printf '%s\n' "$backgrounds/omarchy.png" >"$remembered_png"
run_migration
[[ $(readlink "$current") == "$backgrounds/current.webp" ]] || fail "the current background moves from the removed JPEG to its WebP replacement"
[[ $(<"$call_log") == "-q background set $backgrounds/current.webp" ]] || fail "the running shell is told about the repaired current background"
[[ $(<"$remembered") == "$backgrounds/2-swirl-buck.webp" ]] || fail "an inactive theme's remembered JPEG moves to its bundled WebP replacement"
[[ $(<"$remembered_png") == "$backgrounds/omarchy.webp" ]] || fail "an inactive theme's remembered PNG moves to its bundled WebP replacement"
pass "converted bundled backgrounds keep their current and remembered selections"
run_migration
[[ $(readlink "$current") == "$backgrounds/current.webp" ]] || fail "the repair can be rerun"
[[ $(<"$remembered") == "$backgrounds/2-swirl-buck.webp" ]] || fail "the remembered JPEG repair can be rerun"
[[ $(<"$remembered_png") == "$backgrounds/omarchy.webp" ]] || fail "the remembered PNG repair can be rerun"
pass "the background repair is idempotent"
reset_home
touch "$backgrounds/kept.jpg" "$backgrounds/kept.webp"
ln -s "$backgrounds/kept.jpg" "$current"
printf '%s\n' "$backgrounds/kept.jpg" >"$remembered"
run_migration
[[ $(readlink "$current") == "$backgrounds/kept.jpg" ]] || fail "an existing JPEG background is preserved"
[[ $(<"$remembered") == "$backgrounds/kept.jpg" ]] || fail "an existing remembered JPEG is preserved"
pass "existing backgrounds are not replaced just because a WebP shares their stem"
reset_home
ln -s "$backgrounds/missing.jpeg" "$current"
printf '%s\n' "$backgrounds/missing.png" >"$remembered"
run_migration
[[ $(readlink "$current") == "$backgrounds/missing.jpeg" ]] || fail "a dangling selection without an exact WebP replacement is preserved"
[[ $(<"$remembered") == "$backgrounds/missing.png" ]] || fail "a dangling remembered selection without an exact WebP replacement is preserved"
pass "unrelated dangling background paths are left alone"
reset_home
mkdir -p "$home/.config/omarchy/themes/custom/backgrounds"
touch "$backgrounds/cover.webp" "$home/.config/omarchy/themes/custom/backgrounds/cover.jpg"
custom_remembered="$home/.local/state/omarchy/theme-backgrounds/custom"
printf '%s\n' "$backgrounds/cover.jpg" >"$custom_remembered"
run_migration
[[ $(<"$custom_remembered") == "$backgrounds/cover.jpg" ]] || fail "an inactive custom theme's existing JPEG is not confused with the active theme's WebP"
pass "remembered selections are checked against their named theme"
reset_home
touch "$backgrounds/2-swirl-buck.jpg"
printf '%s\n' "$backgrounds/2-swirl-buck.jpg" >"$remembered"
run_migration
[[ $(<"$remembered") == "$backgrounds/2-swirl-buck.webp" ]] || fail "an active-theme filename collision does not hide an inactive theme's WebP replacement"
pass "active theme files do not control inactive remembered repairs"
reset_home
printf 'tokyo-night\n' >"$home/.local/state/omarchy/current/theme.name"
touch "$backgrounds/2-swirl-buck.jpg"
ln -s "$backgrounds/2-swirl-buck.jpg" "$current"
run_migration
[[ -f $backgrounds/2-swirl-buck.webp ]] || fail "the active staged theme receives its bundled WebP replacement"
[[ $(readlink "$current") == "$backgrounds/2-swirl-buck.webp" ]] || fail "a selected staged JPEG moves before a later refresh can strand it"
pass "the active staged theme is repaired before its old bundled image disappears"
+9
View File
@@ -20,6 +20,15 @@ export OMARCHY_WINDOWS_DIR="$TMPDIR/win"
export HOME="$TMPDIR/home"
mkdir -p "$HOME"
# The command refuses any CPU but x86_64 before defining anything, and sourcing
# it there would exit this test with it. What is under test is the x86_64 path,
# so omarchy-hw-x86 answers as one.
STUB_BIN="$TMPDIR/bin"
mkdir -p "$STUB_BIN"
printf '#!/bin/bash\nexit 0\n' >"$STUB_BIN/omarchy-hw-x86"
chmod +x "$STUB_BIN/omarchy-hw-x86"
PATH="$STUB_BIN:$PATH"
set -- help
source "$ROOT/bin/omarchy-windows-vm" >/dev/null 2>&1
COMPOSE="$OMARCHY_WINDOWS_DIR/docker-compose.yml"
@@ -30,6 +30,13 @@ mount -t tmpfs -o uid=0,gid=0,mode=0710,size=1g home-alice /home/alice
export HOME=/home/alice
unset OMARCHY_WINDOWS_DIR
# The command refuses any CPU but x86_64 before defining anything, and sourcing
# it there would exit this test with it. What is under test is the x86_64 path,
# so omarchy-hw-x86 answers as one.
mkdir -p "$test_tmp/bin"
printf '#!/bin/bash\nexit 0\n' >"$test_tmp/bin/omarchy-hw-x86"
chmod +x "$test_tmp/bin/omarchy-hw-x86"
PATH="$test_tmp/bin:$PATH"
set -- help
source "$test_tmp/omarchy-windows-vm" >/dev/null 2>&1