Files
omarchy/test/shell.d/dictation-test.sh
T
David Heinemeier Hansson c24889096e Add selectable Voxtype and Superwhisper dictation (#14552)
* Add selectable dictation backends

* Move dictation migration after repository priority update

* Extract backend-specific dictation installers

* Keep dictation installer dispatcher executable

* Call dictation backend installers directly

* Drop legacy Superwhisper migration and runtime cleanup

* Separate dictation defaults from generic recording protocol

* Add backend-specific dictation removers

* Preserve existing Voxtype selection during upgrade

* Make dictation backend selection safe and retryable

* Show the dictation indicator only for selected Voxtype

* Allow switching away from stopped Superwhisper

* Remove the built-in bar dictation indicator

* Store default dictation selection under defaults

* Rename dictation backend selector to use

* Move dictation removers into their own menu category
2026-10-08 20:37:26 +02:00

319 lines
14 KiB
Bash

#!/bin/bash
set -euo pipefail
source "$(dirname "${BASH_SOURCE[0]}")/base-test.sh"
test_tmp=$(mktemp -d)
trap 'rm -rf "$test_tmp"' EXIT
export HOME="$test_tmp/home" XDG_CONFIG_HOME="$test_tmp/config" OMARCHY_PATH="$ROOT"
export DICTATION_LOG="$test_tmp/calls" DICTATION_INSTALLED="voxtype superwhisper"
mkdir -p "$test_tmp/bin" "$XDG_CONFIG_HOME/omarchy/defaults"
export PATH="$test_tmp/bin:$ROOT/bin:$PATH"
config="$XDG_CONFIG_HOME/omarchy/defaults/dictation"
cat > "$test_tmp/bin/omarchy-cmd-present" <<'SH'
#!/bin/bash
case "$1" in
omarchy-dictation-*|omarchy-install-dictation-*) command -v "$1" >/dev/null ;;
*) [[ " $DICTATION_INSTALLED " == *" $1 "* ]] ;;
esac
SH
cat > "$test_tmp/bin/omarchy-cmd-missing" <<'SH'
#!/bin/bash
! omarchy-cmd-present "$1"
SH
cat > "$test_tmp/bin/omarchy-launch-floating-terminal-with-presentation" <<'SH'
#!/bin/bash
printf '%s\n' "$*" > "$DICTATION_INSTALL_LOG"
SH
export DICTATION_INSTALL_LOG="$test_tmp/install"
for backend in voxtype superwhisper; do
cat > "$test_tmp/bin/$backend" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >> "$DICTATION_LOG"
if [[ $1 == "status" ]]; then echo "${VOXTYPE_STATUS:-idle}"; exit 0; fi
if [[ -n ${FAIL_SETUP_STEP:-} && $* == "$FAIL_SETUP_STEP" ]]; then exit 7; fi
exit "${DICTATION_EXIT:-0}"
SH
done
for command in omarchy-pkg-add omarchy-notification-send hyprctl omarchy-restart-shell; do
cat > "$test_tmp/bin/$command" <<'SH'
#!/bin/bash
printf '%s %s\n' "${0##*/}" "$*" >> "$DICTATION_LOG"
exit "${SETUP_EXIT:-0}"
SH
done
cat > "$test_tmp/bin/systemctl" <<'SH'
#!/bin/bash
if [[ $* == "--user is-active --quiet superwhisper.service" ]]; then
[[ ${DICTATION_SERVICE_ACTIVE:-1} == 1 ]]
else
printf '%s %s\n' "${0##*/}" "$*" >> "$DICTATION_LOG"
exit "${SERVICE_EXIT:-0}"
fi
SH
cat > "$test_tmp/bin/omarchy-hw-vulkan" <<'SH'
#!/bin/bash
exit 1
SH
cat > "$test_tmp/bin/gum" <<'SH'
#!/bin/bash
exit "${CONFIRM_EXIT:-0}"
SH
cat > "$test_tmp/bin/bash" <<'SH'
#!/bin/bash
if [[ $1 == "/usr/share/superwhisper/setup-user" ]]; then
printf '%s\n' setup-user >> "$DICTATION_LOG"
exit "${SETUP_EXIT:-0}"
else
exec /bin/bash "$@"
fi
SH
chmod +x "$test_tmp/bin/"*
if omarchy-dictation start 2> "$test_tmp/error"; then
fail "unset backend must fail even when providers are installed"
fi
[[ ! -e $DICTATION_LOG ]] || fail "recording never configures or installs a backend"
pass "dictation requires explicit configuration"
omarchy-default-dictation superwhisper
[[ $(cat "$DICTATION_INSTALL_LOG") == "omarchy-install-dictation-superwhisper" ]] || fail "Defaults launches backend installer"
[[ ! -e $config ]] || fail "Defaults does not select before setup succeeds"
if omarchy-default-dictation unknown > "$test_tmp/output" 2>&1; then fail "missing installer must fail"; fi
if omarchy-default-dictation '../invalid' > "$test_tmp/output" 2>&1; then fail "invalid backend must fail"; fi
pass "Defaults delegates setup without prematurely selecting a backend"
for backend in voxtype superwhisper; do
printf '%s\n' "$backend" > "$config"
[[ $(omarchy-default-dictation) == "$backend" ]] || fail "configured backend is readable"
: > "$DICTATION_LOG"
for action in start stop toggle; do omarchy dictation "$action"; done
if [[ $backend == "voxtype" ]]; then
expected=$'voxtype record start\nvoxtype status\nvoxtype record stop\nvoxtype record toggle'
else
expected=$'superwhisper start\nsuperwhisper stop\nsuperwhisper record'
fi
[[ $(cat "$DICTATION_LOG") == "$expected" ]] || fail "adapter dispatches recording for $backend"
done
pass "built-in adapters implement start stop and toggle"
cat > "$test_tmp/bin/omarchy-dictation-future-backend" <<'SH'
#!/bin/bash
[[ ! -e /proc/$$/fd/9 ]] || exit 90
printf '%s\n' "$1" >> "$DICTATION_LOG"
exit "${DICTATION_EXIT:-0}"
SH
chmod +x "$test_tmp/bin/omarchy-dictation-future-backend"
cat > "$test_tmp/bin/omarchy-install-dictation-future-backend" <<'SH'
#!/bin/bash
exit 0
SH
chmod +x "$test_tmp/bin/omarchy-install-dictation-future-backend"
omarchy-default-dictation future-backend
[[ $(cat "$DICTATION_INSTALL_LOG") == "omarchy-install-dictation-future-backend" ]] || fail "third backend installer requires no selector changes"
printf '%s\n' future-backend > "$config"
: > "$DICTATION_LOG"
for action in start stop toggle; do omarchy-dictation "$action"; done
[[ $(cat "$DICTATION_LOG") == $'start\nstop\ntoggle' ]] || fail "third backend requires no dispatcher changes"
result=0
DICTATION_EXIT=7 omarchy-dictation start || result=$?
(( result == 7 )) || fail "backend failures propagate"
rm "$test_tmp/bin/omarchy-dictation-future-backend"
if omarchy-dictation start 2> "$test_tmp/error"; then fail "missing adapter must fail"; fi
printf '%s\n' '../invalid' > "$config"
if omarchy-dictation start 2> "$test_tmp/error"; then fail "invalid backend must fail"; fi
pass "external adapters follow the same protocol and errors propagate"
printf '%s\n' voxtype > "$config"
if SETUP_EXIT=1 omarchy-install-dictation-superwhisper > "$test_tmp/output" 2>&1; then fail "failed installation must fail"; fi
[[ $(cat "$config") == "voxtype" ]] || fail "failed setup must preserve selection"
if DICTATION_EXIT=7 omarchy-install-dictation-superwhisper > "$test_tmp/output" 2>&1; then fail "failed shortcut setup must fail"; fi
[[ $(cat "$config") == "voxtype" ]] || fail "failed shortcuts must preserve selection"
omarchy-install-dictation-superwhisper
[[ $(omarchy-default-dictation) == "superwhisper" ]] || fail "installer selects Superwhisper"
pass "Superwhisper installer owns setup and saves selection only after success"
DICTATION_INSTALLED=superwhisper
CONFIRM_EXIT=1 omarchy-install-dictation-voxtype
[[ $(cat "$config") == "superwhisper" ]] || fail "cancelled installation preserves selection"
omarchy-install-dictation-voxtype
[[ $(omarchy-default-dictation) == "voxtype" ]] || fail "installer selects Voxtype"
DICTATION_INSTALLED="voxtype superwhisper"
: > "$DICTATION_LOG"
omarchy-install-dictation-voxtype
grep -q 'voxtype setup --download --no-post-install' "$DICTATION_LOG" || fail "installed Voxtype retries model setup"
printf '%s\n' 'custom model configuration' > "$XDG_CONFIG_HOME/voxtype/config.toml"
printf '%s\n' superwhisper > "$config"
for step in 'setup --download --no-post-install' 'setup systemd'; do
if FAIL_SETUP_STEP="$step" omarchy-install-dictation-voxtype > "$test_tmp/output" 2>&1; then fail "incomplete setup must fail"; fi
[[ $(cat "$config") == "superwhisper" ]] || fail "incomplete setup preserves selection"
done
if SERVICE_EXIT=1 omarchy-install-dictation-voxtype > "$test_tmp/output" 2>&1; then fail "service failure must fail"; fi
[[ $(cat "$config") == "superwhisper" ]] || fail "service failure preserves selection"
omarchy-install-dictation-voxtype
[[ $(cat "$config") == "voxtype" ]] || fail "retry finishes installed Voxtype setup"
[[ $(cat "$XDG_CONFIG_HOME/voxtype/config.toml") == "custom model configuration" ]] || fail "retry preserves custom configuration"
pass "Voxtype retries incomplete model and service setup without replacing configuration"
: > "$DICTATION_LOG"
if DICTATION_EXIT=7 omarchy-dictation-use superwhisper > "$test_tmp/output" 2>&1; then fail "failed stop must prevent switching"; fi
[[ $(cat "$config") == "voxtype" ]] || fail "failed stop preserves selection"
omarchy-dictation-use superwhisper
[[ $(cat "$DICTATION_LOG") == $'voxtype status\nvoxtype record stop\nvoxtype status\nvoxtype record stop' ]] || fail "switch stops the previous backend"
[[ $(cat "$config") == "superwhisper" ]] || fail "successful stop permits switching"
: > "$DICTATION_LOG"
omarchy-dictation-use superwhisper
[[ ! -s $DICTATION_LOG ]] || fail "reselecting does not interrupt recording"
printf '%s\n' voxtype > "$config"
VOXTYPE_STATUS=stopped omarchy-dictation-use superwhisper
[[ $(cat "$config") == "superwhisper" ]] || fail "a stopped Voxtype daemon permits switching"
printf '%s\n' superwhisper > "$config"
DICTATION_SERVICE_ACTIVE=0 omarchy-dictation-use voxtype
[[ $(cat "$config") == "voxtype" ]] || fail "a stopped Superwhisper service permits switching"
printf '%s\n' removed-backend > "$config"
omarchy-dictation-use voxtype
[[ $(cat "$config") == "voxtype" ]] || fail "a removed adapter permits recovery"
pass "switching stops recordings, preserves failed stops, and recovers unavailable providers"
# Readers may run throughout a switch, but must never observe a truncated file.
(
for ((i = 0; i < 200; i++)); do
selection=$(cat "$config")
[[ $selection == "voxtype" || $selection == "superwhisper" ]] || exit 1
done
) &
reader=$!
for ((i = 0; i < 20; i++)); do
omarchy-dictation-use voxtype
omarchy-dictation-use superwhisper
done
wait "$reader" || fail "concurrent readers always see a complete selection"
printf '%s\n' voxtype > "$config"
pass "selection updates are atomic for concurrent readers"
lua <<'LUA'
local root = os.getenv("ROOT")
local configured, bindings = false, {}
o = {
shell_succeeds = function(command)
assert(command == "omarchy-default-dictation")
return configured
end,
bind = function(...) table.insert(bindings, {...}) end,
}
dofile(root .. "/default/hypr/bindings/dictation.lua")
assert(#bindings == 0, "unconfigured dictation leaves application keys available")
configured = true
dofile(root .. "/default/hypr/bindings/dictation.lua")
assert(#bindings == 5, "any configured backend receives the same bindings")
LUA
pass "dictation shortcuts require a selection without limiting backend names"
lua <<'LUA'
local root = os.getenv("ROOT")
package.path = root .. "/?.lua;" .. package.path
package.loaded["default.hypr.paths"] = { config_home = "/config" }
local selected, available, loaded = "superwhisper", true, {}
local missing = false
o = { cmd_missing = function() return missing end }
local real_open, real_popen, real_dofile = io.open, io.popen, dofile
io.popen = function(command)
assert(command == "omarchy-default-dictation 2>/dev/null")
return { read = function() return selected end, close = function() end }
end
io.open = function(path)
assert(path == "/config/" .. selected .. "/shortcuts.lua")
if available then return { close = function() end } end
end
dofile = function(path) table.insert(loaded, path) end
local function load_backend()
package.loaded["default.hypr.dictation-backend"] = nil
require("default.hypr.dictation-backend")
end
load_backend()
assert(loaded[1] == "/config/superwhisper/shortcuts.lua")
selected, available = "voxtype", false
load_backend()
assert(#loaded == 1, "a backend with no integration should need no config")
selected, available = "future-backend", true
load_backend()
assert(loaded[2] == "/config/future-backend/shortcuts.lua", "integration must not be limited to named providers")
missing = true
load_backend()
assert(#loaded == 2, "an uninstalled backend must not load a stale bridge")
missing = false
dofile = function() error("broken generated bridge") end
load_backend()
assert(#loaded == 2, "a broken bridge must not abort the desktop configuration")
dofile = function(path) table.insert(loaded, path) end
selected = "../../outside"
load_backend()
assert(#loaded == 2, "a backend must not escape the config directory")
selected = nil
load_backend()
assert(#loaded == 2, "no installed backend must leave configuration usable")
io.open, io.popen, dofile = real_open, real_popen, real_dofile
LUA
pass "desktop integration follows the selected backend without personal Hyprland config"
cat > "$test_tmp/bin/systemctl" <<'SH'
#!/bin/bash
exit 0
SH
cat > "$test_tmp/bin/omarchy-pkg-drop" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >> "$DICTATION_LOG"
exit "${REMOVE_EXIT:-0}"
SH
chmod +x "$test_tmp/bin/systemctl" "$test_tmp/bin/omarchy-pkg-drop"
omarchy-remove-dictation-voxtype
[[ ! -e $config ]] || fail "removing Voxtype clears selection"
if omarchy-default-dictation 2> "$test_tmp/error"; then fail "removal must not auto-select another backend"; fi
pass "removal clears selection without automatic fallback"
cat > "$test_tmp/bin/omarchy-shell" <<'SH'
#!/bin/bash
printf '%s\n' "$*" >> "$DICTATION_LOG"
SH
chmod +x "$test_tmp/bin/omarchy-shell"
export XDG_DATA_HOME="$test_tmp/data"
mkdir -p "$HOME/.local/bin" "$XDG_DATA_HOME/superwhisper/app" "$XDG_DATA_HOME/superwhisper/models" "$XDG_CONFIG_HOME/omarchy/plugins" "$HOME/.agents/skills" "$HOME/.claude/skills"
ln -s /usr/bin/superwhisper "$HOME/.local/bin/superwhisper"
ln -s /opt/superwhisper "$XDG_DATA_HOME/superwhisper/app/current"
ln -s /opt/superwhisper/assets/omarchy-plugin/superwhisper-panel "$XDG_CONFIG_HOME/omarchy/plugins/superwhisper-panel"
ln -s /opt/superwhisper/assets/agent-skill/superwhisper "$HOME/.agents/skills/superwhisper"
ln -s /custom/skill "$HOME/.claude/skills/superwhisper"
printf '%s\n' retained > "$XDG_DATA_HOME/superwhisper/models/model"
printf '%s\n' superwhisper > "$config"
if REMOVE_EXIT=1 omarchy-remove-dictation-superwhisper > "$test_tmp/output" 2>&1; then
fail "failed package removal must fail"
fi
[[ -L $HOME/.local/bin/superwhisper && $(cat "$config") == "superwhisper" ]] || fail "failed removal preserves selection and links"
omarchy-remove-dictation-superwhisper
[[ ! -e $config ]] || fail "Superwhisper removal clears its selection"
for link in "$HOME/.local/bin/superwhisper" "$XDG_DATA_HOME/superwhisper/app/current" "$XDG_CONFIG_HOME/omarchy/plugins/superwhisper-panel" "$HOME/.agents/skills/superwhisper"; do
[[ ! -L $link ]] || fail "Superwhisper removal unlinks packaged integration"
done
[[ $(readlink "$HOME/.claude/skills/superwhisper") == "/custom/skill" ]] || fail "custom skills are preserved"
[[ -f $XDG_DATA_HOME/superwhisper/models/model ]] || fail "downloaded models are retained"
printf '%s\n' voxtype > "$config"
omarchy-remove-dictation-superwhisper
[[ $(cat "$config") == "voxtype" ]] || fail "removing Superwhisper preserves another selected backend"
pass "Superwhisper removal clears its selection and owned links while preserving user data and other backends"
cat > "$test_tmp/bin/omarchy-pkg-present" <<'SH'
#!/bin/bash
[[ ${VOXTYPE_PACKAGE:-0} == 1 && $1 == "voxtype-bin" ]]
SH
chmod +x "$test_tmp/bin/omarchy-pkg-present"
rm "$config"
VOXTYPE_PACKAGE=0 /bin/bash -euo pipefail "$ROOT/migrations/1791479273.sh"
[[ ! -e $config ]] || fail "migration does not select an uninstalled backend"
VOXTYPE_PACKAGE=1 /bin/bash -euo pipefail "$ROOT/migrations/1791479273.sh"
[[ $(cat "$config") == "voxtype" ]] || fail "existing Voxtype users keep dictation after upgrade"
printf '%s\n' future-backend > "$config"
VOXTYPE_PACKAGE=1 /bin/bash -euo pipefail "$ROOT/migrations/1791479273.sh"
[[ $(cat "$config") == "future-backend" ]] || fail "upgrade preserves explicit backend selections"
pass "one-time upgrade preserves existing Voxtype without runtime autodetection"