* 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>
161 lines
7.0 KiB
Bash
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
|