Move shell tests under shell runner

This commit is contained in:
David Heinemeier Hansson
2026-05-25 15:16:53 +02:00
parent f05ad16eaf
commit 8ffb498168
4 changed files with 16 additions and 33 deletions
+144
View File
@@ -0,0 +1,144 @@
#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
TMPDIR=""
export PATH="$ROOT/bin:$PATH"
cleanup() {
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT
require_command jq
require_command python3
jq empty "$ROOT/config/omarchy/shell.json"
pass "default shell.json is valid JSON"
jq -e '.version == 1 and (.bar.layout.left | type == "array") and (.bar.layout.center | type == "array") and (.bar.layout.right | type == "array")' "$ROOT/config/omarchy/shell.json" >/dev/null
pass "default shell.json has versioned bar layout"
jq -e '
def ids: map(.id // .);
.bar.layout.center | ids == [
"omarchy.clock",
"omarchy.weather",
"omarchy.system-update",
"omarchy.indicators"
]
' "$ROOT/config/omarchy/shell.json" >/dev/null
pass "default center layout keeps update next to weather"
jq -e '
(.bar.centerAnchor // "") as $anchor |
any(.bar.layout.center[]; (.id // .) == $anchor)
' "$ROOT/config/omarchy/shell.json" >/dev/null
pass "default center anchor exists in center layout"
ROOT="$ROOT" python3 <<'PY'
import json
import os
import sys
from pathlib import Path
root = Path(os.environ["ROOT"])
config = json.loads((root / "config/omarchy/shell.json").read_text())
manifests = {}
for manifest_path in (root / "shell/plugins").glob("**/*.manifest.json"):
data = json.loads(manifest_path.read_text())
manifests[data.get("id", "")] = (manifest_path, data)
for manifest_path in (root / "shell/plugins").glob("**/manifest.json"):
data = json.loads(manifest_path.read_text())
manifests[data.get("id", "")] = (manifest_path, data)
entries = []
for section in ("left", "center", "right"):
entries.extend(config["bar"]["layout"][section])
missing = []
bad = []
for entry in entries:
widget_id = entry["id"] if isinstance(entry, dict) else str(entry)
if not widget_id.startswith("omarchy."):
continue
row = manifests.get(widget_id)
if row is None:
missing.append(widget_id)
continue
manifest_path, manifest = row
if "bar-widget" not in manifest.get("kinds", []):
bad.append(f"{widget_id}: missing bar-widget kind")
entry_point = manifest.get("entryPoints", {}).get("barWidget")
if not entry_point:
bad.append(f"{widget_id}: missing barWidget entry point")
elif not (manifest_path.parent / entry_point).exists():
bad.append(f"{widget_id}: missing {entry_point}")
if missing or bad:
for item in missing:
print(f"missing manifest for {item}", file=sys.stderr)
for item in bad:
print(item, file=sys.stderr)
sys.exit(1)
PY
pass "default bar widget ids resolve to manifests and entry points"
migration=$(grep -rl 'Place the system update indicator next to weather in the bar' "$ROOT/migrations" | head -n 1 || true)
[[ -n $migration ]] || fail "update placement migration exists"
TMPDIR=$(mktemp -d)
mkdir -p "$TMPDIR/home/.config/omarchy"
cat >"$TMPDIR/home/.config/omarchy/shell.json" <<'JSON'
{
"version": 1,
"bar": {
"position": "top",
"transparent": false,
"centerAnchor": "omarchy.clock",
"layout": {
"left": [{ "id": "omarchy.menu" }],
"center": [
{ "id": "omarchy.clock", "format": "HH:mm" },
{ "id": "omarchy.weather", "refreshMinutes": 30 },
{ "id": "omarchy.indicators", "items": ["Dnd", "NightLight"] },
{ "id": "omarchy.system-update", "custom": true }
],
"right": [{ "id": "omarchy.audio" }]
}
},
"plugins": [{ "id": "custom.plugin", "enabled": true }]
}
JSON
HOME="$TMPDIR/home" bash "$migration"
jq -e '
def ids: map(.id // .);
.bar.layout.center | ids == [
"omarchy.clock",
"omarchy.weather",
"omarchy.system-update",
"omarchy.indicators"
]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "update placement migration moves update after weather"
jq -e '
.bar.layout.center[1].refreshMinutes == 30 and
.bar.layout.center[2].custom == true and
.bar.layout.center[3].items == ["Dnd", "NightLight"] and
.plugins == [{ "id": "custom.plugin", "enabled": true }]
' "$TMPDIR/home/.config/omarchy/shell.json" >/dev/null
pass "update placement migration preserves unrelated settings"
before=$(sha256sum "$TMPDIR/home/.config/omarchy/shell.json" | awk '{print $1}')
HOME="$TMPDIR/home" bash "$migration"
after=$(sha256sum "$TMPDIR/home/.config/omarchy/shell.json" | awk '{print $1}')
[[ $before == "$after" ]] || fail "update placement migration is idempotent"
pass "update placement migration is idempotent"
@@ -0,0 +1,157 @@
import QtQuick
import Quickshell
ShellRoot {
id: root
property string resultPath: Quickshell.env("OMARCHY_QML_TEST_RESULT")
property var failures: []
property var commands: []
function fail(message) {
failures.push(String(message))
}
function assertTrue(condition, message) {
if (!condition) fail(message)
}
QtObject {
id: notificationService
property bool doNotDisturb: false
function setDoNotDisturb(value) {
doNotDisturb = !!value
}
}
QtObject {
id: mockShell
function firstPartyServiceFor(id) {
return id === "omarchy.notifications" ? notificationService : null
}
}
QtObject {
id: mockBar
property bool vertical: false
property int barSize: 26
property string fontFamily: "monospace"
property var shell: mockShell
function run(command) {
root.commands.push(String(command))
}
function showTooltip(target, text) {}
function hideTooltip(target) {}
function registerClickTarget(target) {}
function unregisterClickTarget(target) {}
}
QtObject {
id: indicatorHost
property bool revealInactiveIndicators: true
}
function createIndicator(name) {
var component = Qt.createComponent("file://" + rootPath + "/shell/plugins/bar/indicators/" + name + ".qml")
if (component.status !== Component.Ready) {
fail(name + " failed to load: " + component.errorString())
return null
}
var item = component.createObject(root, {
indicatorHost: indicatorHost,
indicatorBlock: "inactive",
activeOverride: null
})
if (!item) {
fail(name + " failed to instantiate: " + component.errorString())
return null
}
return item
}
function injectBar(item) {
assertTrue(item.bar === null || item.bar === undefined, item.moduleName + " starts without bar")
item.bar = mockBar
assertTrue(item.bar === mockBar, item.moduleName + " accepts delayed bar injection")
}
function commandCount(command) {
var count = 0
for (var i = 0; i < commands.length; i++) {
if (commands[i] === command) count++
}
return count
}
function writeResult() {
var payload = JSON.stringify({
ok: failures.length === 0,
failures: failures,
commands: commands,
dnd: notificationService.doNotDisturb
})
if (resultPath) {
Quickshell.execDetached(["bash", "-lc", "printf '%s' " + shellQuote(payload) + " > " + shellQuote(resultPath)])
}
}
function shellQuote(value) {
return "'" + String(value).replace(/'/g, "'\\''") + "'"
}
readonly property string rootPath: Quickshell.env("OMARCHY_PATH")
Timer {
interval: 1
running: true
repeat: false
onTriggered: {
var dnd = root.createIndicator("Dnd")
if (dnd) {
dnd.moduleName = "Dnd"
root.injectBar(dnd)
notificationService.doNotDisturb = false
dnd.triggerPress(Qt.LeftButton)
root.assertTrue(notificationService.doNotDisturb === true, "DND left click toggles notification service")
}
var nightLight = root.createIndicator("NightLight")
if (nightLight) {
nightLight.moduleName = "NightLight"
root.injectBar(nightLight)
nightLight.triggerPress(Qt.LeftButton)
root.assertTrue(root.commandCount("omarchy-toggle-nightlight") === 1, "Night Light left click runs toggle command")
}
var screenRecording = root.createIndicator("ScreenRecording")
if (screenRecording) {
screenRecording.moduleName = "ScreenRecording"
root.injectBar(screenRecording)
screenRecording.triggerPress(Qt.LeftButton)
root.assertTrue(root.commandCount("omarchy-capture-screenrecording") === 1, "Screen Recording left click runs capture command")
}
var dictation = root.createIndicator("Dictation")
if (dictation) {
dictation.moduleName = "Dictation"
root.injectBar(dictation)
dictation.triggerPress(Qt.LeftButton)
dictation.triggerPress(Qt.RightButton)
root.assertTrue(root.commandCount("omarchy-voxtype-model") === 1, "Dictation left click runs model command")
root.assertTrue(root.commandCount("omarchy-voxtype-config") === 1, "Dictation right click runs config command")
}
var stayAwake = root.createIndicator("StayAwake")
if (stayAwake) {
stayAwake.moduleName = "StayAwake"
root.injectBar(stayAwake)
stayAwake.triggerPress(Qt.LeftButton)
root.assertTrue(root.commandCount("omarchy-toggle-idle") === 1, "Stay Awake left click runs idle toggle command")
}
root.writeResult()
}
}
}
+66
View File
@@ -0,0 +1,66 @@
#!/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
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping QML contract test"
exit 0
fi
require_command jq
TMPDIR=$(mktemp -d)
result="$TMPDIR/result.json"
log="$TMPDIR/quickshell.log"
config_dir="$TMPDIR/indicator-contract"
mkdir -p "$config_dir" "$TMPDIR/home"
cp "$SHELL_TEST_DIR/fixtures/indicator-contract/shell.qml" "$config_dir/shell.qml"
ln -s "$ROOT/shell/Ui" "$config_dir/Ui"
ln -s "$ROOT/shell/Commons" "$config_dir/Commons"
OMARCHY_PATH="$ROOT" \
OMARCHY_QML_TEST_RESULT="$result" \
HOME="$TMPDIR/home" \
QML2_IMPORT_PATH="$ROOT/shell${QML2_IMPORT_PATH:+:$QML2_IMPORT_PATH}" \
QML_IMPORT_PATH="$ROOT/shell${QML_IMPORT_PATH:+:$QML_IMPORT_PATH}" \
PATH="$ROOT/bin:$PATH" \
quickshell -p "$config_dir" --no-color >"$log" 2>&1 &
QS_PID=$!
for _ in {1..50}; do
[[ -s $result ]] && break
if ! kill -0 "$QS_PID" 2>/dev/null; then
sed -n '1,160p' "$log" >&2
fail "QML contract quickshell exited before writing result"
fi
sleep 0.1
done
[[ -s $result ]] || {
sed -n '1,160p' "$log" >&2
fail "QML contract test timed out"
}
if ! jq -e '.ok == true' "$result" >/dev/null; then
printf 'QML contract result:\n' >&2
jq . "$result" >&2
printf 'QML contract log:\n' >&2
sed -n '1,160p' "$log" >&2
fail "QML indicator contract checks pass"
fi
pass "QML indicator contract checks pass"
+93
View File
@@ -0,0 +1,93 @@
#!/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
[[ -n $TMPDIR && -d $TMPDIR ]] && rm -rf "$TMPDIR"
}
trap cleanup EXIT
if [[ -z ${WAYLAND_DISPLAY:-} ]]; then
pass "no Wayland compositor; skipping shell runtime smoke test"
exit 0
fi
if ! command -v quickshell >/dev/null 2>&1; then
pass "quickshell not installed; skipping shell runtime smoke test"
exit 0
fi
require_command jq
TMPDIR=$(mktemp -d)
test_root="$TMPDIR/omarchy"
test_home="$TMPDIR/home"
stub_bin="$TMPDIR/bin"
log="$TMPDIR/quickshell.log"
mkdir -p "$test_root" "$test_home" "$stub_bin"
cp -a "$ROOT/shell" "$test_root/shell"
ln -s "$ROOT/config" "$test_root/config"
ln -s "$ROOT/bin" "$test_root/bin"
cat >"$stub_bin/omarchy-update-available" <<'SH'
#!/bin/bash
echo "Omarchy update available (test)"
exit 0
SH
chmod +x "$stub_bin/omarchy-update-available"
OMARCHY_PATH="$test_root" \
HOME="$test_home" \
PATH="$stub_bin:$ROOT/bin:$PATH" \
quickshell -p "$test_root/shell" --no-color >"$log" 2>&1 &
QS_PID=$!
for _ in {1..80}; do
if OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-shell" -q shell ping >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
sed -n '1,200p' "$log" >&2
fail "test shell exited before IPC became available"
fi
sleep 0.1
done
OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-shell" -q omarchy.system-update refresh >/dev/null 2>&1 || true
sleep 0.8
geometry=$(OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-shell" shell debugBarGeometry)
if [[ -z $geometry ]]; then
sed -n '1,200p' "$log" >&2
fail "debug bar geometry returned output"
fi
jq -e '
map(select(.section == "center" and .visible == true)) as $center |
($center | map(select(.id == "omarchy.weather" and .visible == true)) | length) >= 1 and
($center | map(select(.id == "omarchy.system-update" and .visible == true and .width > 0)) | length) >= 1 and
($center | map(select(.id == "omarchy.indicators" and .visible == true)) | length) >= 1 and
(
($center | map(select(.id == "omarchy.weather")) | first | .x) <
($center | map(select(.id == "omarchy.system-update")) | first | .x)
) and
(
($center | map(select(.id == "omarchy.system-update")) | first | .x) <
($center | map(select(.id == "omarchy.indicators")) | first | .x)
)
' <<<"$geometry" >/dev/null || {
printf 'Geometry:\n%s\n' "$geometry" | jq . >&2
sed -n '1,200p' "$log" >&2
fail "runtime geometry places visible update between weather and indicators"
}
pass "runtime geometry places visible update between weather and indicators"