Files
omarchycn/test/shell.d/runtime-smoke-test.sh
T
David Heinemeier HanssonandClaude Opus 5 64810d815b Load each bar widget's component once
Qt.createComponent is asynchronous, but a widget was only recorded in
pluginWidgetComponents once its component finished. syncPluginWidgets runs
several times while the shell starts, and every pass that ran while a load
was still in flight could not tell it apart from one that had never
happened — so it started a second load of the same URL. All twenty bar
widgets were loaded twice.

The duplicate component then replaced the first in the registry, and
swapping a slot's component makes its Loader build the replacement before
dropping the original. Both were briefly live, and both registered the
widget's IPC handler, which is where the "another handler is registered
for target" warnings came from.

Claim the key when the load starts instead, and release it if the
component fails so a later rescan can retry.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
2026-07-26 22:19:57 -07:00

274 lines
9.5 KiB
Bash
Executable File

#!/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"
return 0
}
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
shell_ipc() {
OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-shell" "$@"
}
shell_ipc_quiet() {
OMARCHY_PATH="$test_root" "$ROOT/bin/omarchy-shell" -q "$@"
}
fail_with_log() {
local description="$1"
sed -n '1,240p' "$log" >&2
fail "$description"
}
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"
cat >"$stub_bin/curl" <<'SH'
#!/bin/bash
case "${*: -1}" in
*'?format=j1')
printf '{"current_condition":[{"weatherCode":"113","temp_F":"72"}]}\n'
;;
*'?format=%l')
printf 'Test City, Test Region\n'
;;
*)
exit 1
;;
esac
SH
chmod +x "$stub_bin/curl"
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 shell_ipc_quiet shell ping >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited before IPC became available"
fi
sleep 0.1
done
plugins=""
for _ in {1..80}; do
plugins=$(shell_ipc shell listPlugins 2>/dev/null || true)
if jq -e 'length > 0' <<<"$plugins" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited before plugins were listed"
fi
sleep 0.1
done
jq -e '
map(.id) as $ids |
all(["omarchy.menu", "omarchy.notifications", "omarchy.clock", "omarchy.osd"][]; $ids | index(.)) and
all(.[]; (.kinds | type == "array") and (.enabled | type == "boolean") and (.firstParty | type == "boolean"))
' <<<"$plugins" >/dev/null || {
printf 'Plugins:\n%s\n' "$plugins" | jq . >&2
fail_with_log "shell IPC lists plugin metadata"
}
pass "shell IPC lists plugin metadata"
shell_config=$(shell_ipc shell listShellConfig)
jq -e '
.version == 1 and
(.bar.layout.left | type == "array") and
(.bar.layout.center | type == "array") and
(.bar.layout.right | type == "array")
' <<<"$shell_config" >/dev/null || {
printf 'Shell config:\n%s\n' "$shell_config" | jq . >&2
fail_with_log "shell IPC returns effective shell config"
}
pass "shell IPC returns effective shell config"
[[ $(shell_ipc shell summon omarchy.menu '{"menu":"apps"}') == "ok" ]] || fail_with_log "shell IPC summons menu apps overlay"
shell_ipc_quiet shell hide omarchy.menu >/dev/null
[[ $(shell_ipc shell summon missing.plugin "{}") == "unknown" ]] || fail_with_log "shell IPC rejects unknown plugin"
pass "shell IPC summon and hide contract works"
[[ $(shell_ipc notifications ping) == "ok" ]] || fail_with_log "notifications IPC responds"
[[ $(shell_ipc notifications setDnd false) == "off" ]] || fail_with_log "notifications IPC toggles DND"
[[ $(shell_ipc media ping) == "ok" ]] || fail_with_log "media IPC responds"
jq -e '.hasPlayer | type == "boolean"' <<<"$(shell_ipc media status)" >/dev/null || fail_with_log "media IPC returns status JSON"
jq -e '.enabled | type == "boolean"' <<<"$(shell_ipc idle status)" >/dev/null || fail_with_log "idle IPC returns status JSON"
jq -e '.locked | type == "boolean"' <<<"$(shell_ipc lock status)" >/dev/null || fail_with_log "lock IPC returns status JSON"
[[ $(shell_ipc image-selector ping) == "ok" ]] || fail_with_log "image selector IPC responds"
[[ $(shell_ipc osd ping) == "ok" ]] || fail_with_log "OSD IPC responds"
[[ $(shell_ipc osd show '{"message":"Runtime smoke","duration":0}') == "ok" ]] || fail_with_log "OSD IPC opens"
[[ $(shell_ipc osd close) == "ok" ]] || fail_with_log "OSD IPC closes"
pass "plugin IPC contracts respond"
shell_ipc_quiet shell rescanPlugins >/dev/null
selector_rows_b64=$(printf '%s\t%s' "$TMPDIR/selector.png" "$TMPDIR/selector.png" | base64 -w 0)
selector_selection_file=$(mktemp "$TMPDIR/selector-selection.XXXXXX")
selector_done_file=$(mktemp "$TMPDIR/selector-done.XXXXXX")
rm -f "$selector_done_file"
selector_open=""
for _ in {1..80}; do
selector_open=$(shell_ipc image-selector open "" "$selector_rows_b64" "" "$selector_selection_file" "$selector_done_file" false false 2>/dev/null || true)
if [[ $selector_open == "ok" ]]; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited during plugin rescan"
fi
sleep 0.1
done
[[ $selector_open == "ok" ]] || fail_with_log "image selector IPC survives plugin rescan"
shell_ipc_quiet image-selector cancel "$selector_done_file" >/dev/null
rm -f "$selector_selection_file" "$selector_done_file"
pass "image selector IPC survives plugin rescan"
shell_ipc_quiet omarchy.system-update refresh >/dev/null 2>&1 || true
sleep 0.8
default_ids=$(jq -c '(.bar.layout.left + .bar.layout.center + .bar.layout.right) | map(.id // .)' "$ROOT/config/omarchy/shell.json")
visible_default_ids='[
"omarchy.menu",
"omarchy.workspaces",
"omarchy.clock",
"omarchy.weather",
"omarchy.system-update",
"omarchy.network",
"omarchy.audio",
"omarchy.monitor"
]'
geometry=""
for _ in {1..80}; do
geometry=$(shell_ipc shell debugBarGeometry 2>/dev/null || true)
if jq -e --argjson expected "$default_ids" '
. as $rows | all($expected[]; . as $id | any($rows[]; .id == $id))
' <<<"$geometry" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited before default bar geometry settled"
fi
sleep 0.1
done
if [[ -z $geometry ]]; then
fail_with_log "debug bar geometry returned output"
fi
jq -e --argjson expected "$default_ids" --argjson visibleExpected "$visible_default_ids" '
. as $rows |
all($expected[]; . as $id | any($rows[]; .id == $id)) and
all($visibleExpected[]; . as $id | any($rows[]; .id == $id and .visible == true and .width > 0 and .height > 0))
' <<<"$geometry" >/dev/null || {
printf 'Geometry:\n' >&2
jq . <<<"$geometry" >&2
fail_with_log "default bar layout renders expected module slots"
}
pass "default bar layout renders expected module slots"
jq -e '
map(select(.section == "center")) | map(.id) as $center |
($center | index("omarchy.weather")) != null and
($center | index("omarchy.system-update")) != null and
($center | index("omarchy.indicators")) != null and
(($center | index("omarchy.weather")) < ($center | index("omarchy.system-update"))) and
(($center | index("omarchy.system-update")) < ($center | index("omarchy.indicators")))
' <<<"$geometry" >/dev/null || {
printf 'Geometry:\n' >&2
jq . <<<"$geometry" >&2
fail_with_log "runtime geometry keeps update before indicators"
}
pass "runtime geometry keeps update before indicators"
for panel_id in omarchy.audio omarchy.bluetooth omarchy.monitor omarchy.network omarchy.power; do
shell_ipc "$panel_id" open >/dev/null || fail_with_log "direct panel IPC opens $panel_id"
shell_ipc "$panel_id" close >/dev/null || fail_with_log "direct panel IPC closes $panel_id"
done
pass "direct panel IPC opens and closes default panels"
# Each widget registers its IPC handler once while the shell starts. A second
# registration for the same target means two instances of that widget were
# live at once, which is what duplicate component loads produced: every sync
# pass that ran while a widget's asynchronous load was still in flight started
# a second load, and swapping a slot's component builds the replacement before
# dropping the original. Checked before the reload below, which rebuilds
# widgets by design.
if grep -q "another handler is registered for target" "$log"; then
grep "another handler is registered for target" "$log" | sed 's/^/ /' | head -20 >&2
fail_with_log "each widget registers its IPC handler once while starting"
fi
pass "each widget registers its IPC handler once while starting"
HOME="$test_home" OMARCHY_PATH="$test_root" PATH="$ROOT/bin:$PATH" "$ROOT/bin/omarchy-bar-plugin" remove omarchy.audio
for _ in {1..80}; do
shell_config=$(shell_ipc shell listShellConfig 2>/dev/null || true)
geometry=$(shell_ipc shell debugBarGeometry 2>/dev/null || true)
if jq -e 'all(.bar.layout.right[]; (.id // .) != "omarchy.audio")' <<<"$shell_config" >/dev/null 2>&1 && \
jq -e 'all(.[]; .id != "omarchy.audio")' <<<"$geometry" >/dev/null 2>&1; then
break
fi
if ! kill -0 "$QS_PID" 2>/dev/null; then
fail_with_log "test shell exited before reloaded bar geometry settled"
fi
sleep 0.1
done
jq -e 'all(.bar.layout.right[]; (.id // .) != "omarchy.audio")' <<<"$shell_config" >/dev/null || {
printf 'Shell config after reload:\n%s\n' "$shell_config" | jq . >&2
fail_with_log "bar remove reloads shell config"
}
jq -e 'all(.[]; .id != "omarchy.audio")' <<<"$geometry" >/dev/null || {
printf 'Geometry after reload:\n' >&2
jq . <<<"$geometry" >&2
fail_with_log "runtime bar layout updates after shell config reload"
}
pass "bar remove reloads shell config and updates bar layout"