Files
omarchy/test/shell.d/shell-ipc-socket-test.sh
T
David Heinemeier HanssonandClaude Opus 5.5 82979b9a18 Remove the IPC socket of each shell a test starts (#13443)
* Answer the Elsewhen panel over the shell's IPC socket

The Elsewhen panel arrived with a plain IpcHandler, so omarchy-shell
reached it only through the slower qs ipc fallback, and the test that
every first-party handler registers as ShellIpc failed on quattro.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

* Remove the IPC socket of each shell a test starts

The runtime smoke and screenshot sanity tests start the real shell from a
temporary copy and kill it, which leaves its IPC socket behind in
XDG_RUNTIME_DIR. A session that runs the suite a few times collected
dozens of them.

base-test.sh gains shell_ipc_socket, which derives a shell's socket as
omarchy-shell does, and both tests remove theirs in cleanup. The socket
test derives its fixture's socket through it too, so the helper cannot
drift from omarchy-shell.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>

---------

Co-authored-by: Claude Opus 5.5 <noreply@anthropic.com>
2026-09-27 21:57:46 +02:00

161 lines
7.0 KiB
Bash

#!/bin/bash
set -euo pipefail
source "$(cd -- "$(dirname -- "${BASH_SOURCE[0]}")" && pwd)/base-test.sh"
require_command socat
test_tmp=$(mktemp -d)
server_pid=""
cleanup() {
[[ -n $server_pid ]] && kill "$server_pid" 2>/dev/null || true
rm -rf "$test_tmp"
}
trap cleanup EXIT
root_dir="$test_tmp/root"
run_dir="$test_tmp/run"
stub_bin="$test_tmp/bin"
mkdir -p "$root_dir/shell" "$run_dir" "$stub_bin"
touch "$root_dir/shell/shell.qml"
# The shell names its socket after its config and display, as omarchy-shell
# derives it.
display="test-display"
socket=$(XDG_RUNTIME_DIR="$run_dir" WAYLAND_DISPLAY="$display" shell_ipc_socket "$root_dir")
# qs is the fallback: it records that it ran and answers.
cat >"$stub_bin/qs" <<'SH'
#!/bin/bash
printf 'qs\n' >>"$QS_CALLS"
printf 'from-qs\n'
SH
chmod +x "$stub_bin/qs"
# The fake shell replies as the mode file says, and logs each request with
# its separators made visible.
cat >"$test_tmp/responder" <<'SH'
#!/bin/bash
IFS= read -r -d $'\x1e' request
printf '%s\n' "${request//$'\x1f'/|}" >>"$REQUESTS"
case $(<"$MODE") in
ok) printf 'OK\x1f%s\x1e' "answer to ${request//$'\x1f'/ }" ;;
newlines) printf 'OK\x1fline\n\n\x1e' ;;
skip) printf 'SKIP\x1e' ;;
hang) sleep 2 ;;
close) ;;
partial) printf 'OK\x1fpartial answer' ;;
esac
SH
chmod +x "$test_tmp/responder"
mode="$test_tmp/mode"
calls="$test_tmp/qs-calls"
requests="$test_tmp/requests"
MODE="$mode" REQUESTS="$requests" socat UNIX-LISTEN:"$socket",fork SYSTEM:"$test_tmp/responder" 2>/dev/null &
server_pid=$!
for _ in $(seq 50); do [[ -S $socket ]] && break; sleep 0.05; done
shell_call() {
: >"$calls"
PATH="$stub_bin:$PATH" OMARCHY_PATH="$root_dir" XDG_RUNTIME_DIR="$run_dir" WAYLAND_DISPLAY="$display" QS_CALLS="$calls" \
OMARCHY_SHELL_IPC_TIMEOUT="${IPC_TIMEOUT:-2s}" "$ROOT/bin/omarchy-shell" "$@"
}
printf 'ok' >"$mode"
output=$(shell_call media next extra-arg)
[[ $output == "answer to media next extra-arg" ]] || fail "the shell answers over its socket" "got: $output"
[[ ! -s $calls ]] || fail "an answered call never starts qs ipc"
[[ $(tail -n1 "$requests") == "media|next|extra-arg" ]] || fail "arguments travel as separate fields" "got: $(tail -n1 "$requests")"
pass "the shell answers over its socket without starting qs ipc"
output=$(shell_call -q media next)
[[ -z $output ]] || fail "quiet calls print nothing" "got: $output"
pass "quiet calls over the socket print nothing"
printf 'newlines' >"$mode"
output=$(shell_call shell ping)
[[ $output == "line" ]] || fail "socket output drops trailing newlines as qs ipc's does" "got: $(printf '%q' "$output")"
pass "socket output drops trailing newlines as the qs ipc path does"
printf 'skip' >"$mode"
output=$(shell_call nosuch ping)
[[ $output == "from-qs" && -s $calls ]] || fail "a call the shell did not run goes to qs ipc" "got: $output"
pass "a call the shell did not run goes to qs ipc for its exact answer"
printf 'ok' >"$mode"
: >"$requests"
output=$(shell_call shell summon "$(printf 'a\x1fb')")
[[ $output == "from-qs" && ! -s $requests ]] || fail "an argument holding a separator goes straight to qs ipc" "got: $output"
pass "an argument holding a separator goes straight to qs ipc"
printf 'hang' >"$mode"
output=$(IPC_TIMEOUT=0.5s shell_call shell ping 2>&1) && fail "a shell that never replies fails the call"
[[ $output == "omarchy-shell is not responding" && ! -s $calls ]] ||
fail "a call that may have run is reported, not retried through qs ipc" "got: $output"
pass "a call that may have run is reported as unresponsive, not retried"
printf 'close' >"$mode"
output=$(shell_call shell ping 2>&1) && fail "a shell that closes without answering fails the call"
[[ $output == "omarchy-shell is not responding" && ! -s $calls ]] ||
fail "a connection closed without an answer is reported, not retried" "got: $output"
pass "a connection closed without an answer is reported, not retried"
printf 'partial' >"$mode"
output=$(shell_call shell listPlugins 2>&1) && fail "a reply cut off before its end fails the call"
[[ $output == "omarchy-shell is not responding" && ! -s $calls ]] ||
fail "a reply cut off before its end is reported, not taken or retried" "got: $output"
pass "a reply cut off before its end is reported, not taken or retried"
# SIGKILL leaves the socket file behind with nothing listening, as a crashed
# shell does; socat then never connects, so qs ipc may safely answer.
kill -9 "$server_pid"
wait "$server_pid" 2>/dev/null || true
server_pid=""
[[ -S $socket ]] || fail "a killed listener leaves its socket file behind"
output=$(shell_call shell ping)
[[ $output == "from-qs" ]] || fail "a stale socket with nothing listening falls back to qs ipc" "got: $output"
pass "a stale socket with nothing listening falls back to qs ipc"
rm -f "$socket"
output=$(shell_call shell ping)
[[ $output == "from-qs" ]] || fail "without the socket calls go through qs ipc" "got: $output"
pass "without the socket calls go through qs ipc"
# A call for a different display never reaches this shell's socket.
printf 'ok' >"$mode"
MODE="$mode" REQUESTS="$requests" socat UNIX-LISTEN:"$socket",fork SYSTEM:"$test_tmp/responder" 2>/dev/null &
server_pid=$!
for _ in $(seq 50); do [[ -S $socket ]] && break; sleep 0.05; done
output=$(display="other-display" shell_call shell ping)
[[ $output == "from-qs" ]] || fail "a call for another display does not reach this shell's socket" "got: $output"
pass "a call for another display does not reach this shell's socket"
run_node_test <<'JS'
const fs = require('fs')
const { execSync } = require('child_process')
const shellQml = fs.readFileSync(path.join(root, 'shell/shell.qml'), 'utf8')
const registry = fs.readFileSync(path.join(root, 'shell/Commons/IpcRegistry.qml'), 'utf8')
// Only handlers that register can answer over the socket, so every
// first-party one does; the registry's own bare handler is the reference for
// the functions a handler never exposes.
const bare = execSync(`grep -rln 'IpcHandler {' ${path.join(root, 'shell')} --include=*.qml || true`).toString().trim().split('\n').filter(Boolean)
assertDeepEqual(bare.map((f) => path.relative(root, f)).sort(), ['shell/Commons/IpcRegistry.qml', 'shell/Commons/ShellIpc.qml'], 'first-party IPC handlers register through ShellIpc')
// Declared functions are allowed by name: destroy() and other QObject
// methods are callable yet not enumerable, so subtracting builtins from what a
// call can reach would let them through.
assert(
registry.includes('readonly property var builtins: functionNames(bareHandler)') &&
/return builtins\.indexOf\(name\) === -1 && !\/Changed\$\/\.test\(name\)/.test(registry) &&
registry.includes('if (declaredFunctions(handler).indexOf(method) === -1) return { ran: false }') &&
registry.includes('handler[method].length !== args.length'),
'the socket answers only declared functions with their exact argument count'
)
assert(
shellQml.includes('+ Qt.md5(shell.omarchyPath + "/shell\\n" + Quickshell.env("WAYLAND_DISPLAY")).slice(0, 16) + ".sock"'),
'the shell names its socket after its config and display, as omarchy-shell does'
)
JS