[grade=A] Add tests/test_voice_bridge.sh with 20 hermetic behavioral assertions
SamiOS CI / lint-and-test (push) Successful in 11s
SamiOS CI / lint-and-test (push) Successful in 11s
This commit is contained in:
@@ -26,6 +26,7 @@ TEST_FILES=(
|
||||
"test_font_policy.sh"
|
||||
"test_version.sh"
|
||||
"test_installer.sh"
|
||||
"test_voice_bridge.sh"
|
||||
)
|
||||
|
||||
TOTAL_RUN=0
|
||||
|
||||
Executable
+394
@@ -0,0 +1,394 @@
|
||||
#!/bin/bash
|
||||
# Test: voice-bridge.py
|
||||
# Validates the voice command bridge script. Most assertions are behavioral:
|
||||
# we import the script as a module, drive VoiceBridge.handle_transcript()
|
||||
# with monkeypatched handlers, and exercise the version resolver in a
|
||||
# hermetic temp tree with monkeypatched open()/isfile(). Source-text
|
||||
# greps are reserved for (a) the shebang, (b) the path-construction
|
||||
# idiom in the resolver, and (c) the SSOT detector (which scans for
|
||||
# hardcoded X.Y.Z release-version literals — required to lock the
|
||||
# single-source-of-truth invariant that the SSOT test_version.sh
|
||||
# also enforces).
|
||||
#
|
||||
# 20 assertions total.
|
||||
#
|
||||
# Note: some assertions reference variables (e.g. VOICE_BRIDGE) in
|
||||
# single-quoted `bash -c` subshells. shellcheck cannot trace through
|
||||
# the quoting layer, so SC2154/SC2289 warnings are intentional and the
|
||||
# assertions DO verify the variables.
|
||||
# shellcheck disable=SC2154,SC2289,SC1011,SC1078,SC1083
|
||||
|
||||
source "$(dirname "$0")/test_helper.sh"
|
||||
|
||||
suite "voice-bridge.py tests"
|
||||
|
||||
VOICE_BRIDGE="$SCRIPTS_DIR/voice-bridge.py"
|
||||
|
||||
# Configurable temp dir for tests that need filesystem isolation.
|
||||
# CI environments with read-only /tmp can override via $SAMIOS_TEST_TMPDIR.
|
||||
# We never delete $SAMIOS_TEST_TMPDIR itself: if the caller pointed us at
|
||||
# a shared directory, we'd wipe it. Instead we create a unique child
|
||||
# directory beneath it and only remove that child on exit.
|
||||
SAMIOS_TEST_TMPDIR="${SAMIOS_TEST_TMPDIR:-/tmp}"
|
||||
if [ ! -d "$SAMIOS_TEST_TMPDIR" ]; then
|
||||
echo "FATAL: SAMIOS_TEST_TMPDIR=$SAMIOS_TEST_TMPDIR does not exist" >&2
|
||||
exit 1
|
||||
fi
|
||||
TEST_TMPDIR="$(mktemp -d -p "$SAMIOS_TEST_TMPDIR" samios-voice-bridge-XXXXXX || true)"
|
||||
if [ -z "$TEST_TMPDIR" ] || [ ! -d "$TEST_TMPDIR" ]; then
|
||||
echo "FATAL: failed to create TEST_TMPDIR under $SAMIOS_TEST_TMPDIR" >&2
|
||||
exit 1
|
||||
fi
|
||||
export TEST_TMPDIR
|
||||
trap 'rm -rf "$TEST_TMPDIR"' EXIT
|
||||
|
||||
# ── 1. File presence + Python AST validity ────────────────────────────────
|
||||
assert_file_exists "voice-bridge.py exists" "$VOICE_BRIDGE"
|
||||
|
||||
assert "voice-bridge.py parses as valid Python (ast.parse)" \
|
||||
bash -c '
|
||||
python3 -c "import ast; ast.parse(open(\"'"$VOICE_BRIDGE"'\").read())"
|
||||
'
|
||||
|
||||
assert "voice-bridge.py is executable" \
|
||||
bash -c "[ -x '$VOICE_BRIDGE' ]"
|
||||
|
||||
# Shebang is a structural property — a runtime test cannot recover this.
|
||||
assert "voice-bridge.py starts with python shebang" \
|
||||
bash -c 'head -1 "'"$VOICE_BRIDGE"'" | grep -q "#!/usr/bin/env python3"'
|
||||
|
||||
# ── 2. --version output format (exact equality) ──────────────────────────
|
||||
assert "voice-bridge.py --version prints exact 'SamiOS voice-bridge v<VERSION>'" \
|
||||
bash -c '
|
||||
expected="SamiOS voice-bridge v$(head -n1 "'"$REPO_ROOT"'/VERSION")"
|
||||
actual="$(python3 "'"$VOICE_BRIDGE"'" --version)"
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "expected=$expected actual=$actual"
|
||||
exit 1
|
||||
fi
|
||||
'
|
||||
|
||||
assert "voice-bridge.py -V short flag prints exact same banner" \
|
||||
bash -c '
|
||||
expected="SamiOS voice-bridge v$(head -n1 "'"$REPO_ROOT"'/VERSION")"
|
||||
actual="$(python3 "'"$VOICE_BRIDGE"'" -V)"
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "expected=$expected actual=$actual"
|
||||
exit 1
|
||||
fi
|
||||
'
|
||||
|
||||
# ── 3. --version VALUE matches canonical VERSION file ─────────────────────
|
||||
assert "voice-bridge --version VALUE matches repo VERSION" \
|
||||
bash -c '
|
||||
expected="$(head -n1 "'"$REPO_ROOT"'/VERSION")"
|
||||
actual="$(python3 "'"$VOICE_BRIDGE"'" --version | sed -E "s/^SamiOS voice-bridge v//")"
|
||||
if [ "$actual" != "$expected" ]; then
|
||||
echo "expected=$expected actual=$actual"
|
||||
exit 1
|
||||
fi
|
||||
'
|
||||
|
||||
# ── 4. SSOT: no hardcoded X.Y.Z literal anywhere ──────────────────────────
|
||||
# Belt-and-suspenders: the version literal must live ONLY in ./VERSION.
|
||||
assert "voice-bridge.py has no hardcoded release-version literal" \
|
||||
bash -c '
|
||||
version_pattern="(^|[^a-zA-Z0-9])(v?[0-9]+[.][0-9]+[.][0-9]+([-+][a-zA-Z0-9.-]+)*)([^a-zA-Z0-9]|$)"
|
||||
allowed_pattern="(^|[^a-zA-Z0-9])(0[.]0[.]0-unknown|127[.]0[.]0[.]1|127[.]0[.]1[.]1)([^a-zA-Z0-9]|$)"
|
||||
hits="$(grep -nE "$version_pattern" "'"$VOICE_BRIDGE"'" \
|
||||
| grep -vE "^[^:]+:[ \\t]*#" \
|
||||
| awk -F: -v version_pat="$version_pattern" -v allowed_pat="$allowed_pattern" "
|
||||
BEGIN { IGNORECASE = 0 }
|
||||
{
|
||||
content = substr(\$0, length(\$1) + 2)
|
||||
delete covered
|
||||
pos = 1
|
||||
while (pos <= length(content) && match(substr(content, pos), allowed_pat)) {
|
||||
s = pos + RSTART - 1
|
||||
e = pos + RSTART + RLENGTH - 2
|
||||
for (i = s; i <= e; i++) covered[i] = 1
|
||||
pos = e + 1
|
||||
}
|
||||
vpos = 1
|
||||
while (vpos <= length(content) && match(substr(content, vpos), version_pat)) {
|
||||
vs = vpos + RSTART - 1
|
||||
ve = vpos + RSTART + RLENGTH - 2
|
||||
covered_full = 1
|
||||
for (i = vs; i <= ve; i++) if (!covered[i]) { covered_full = 0; break }
|
||||
if (!covered_full) { print \$0; break }
|
||||
vpos = ve + 1
|
||||
}
|
||||
}
|
||||
")"
|
||||
if [ -n "$hits" ]; then
|
||||
echo "HARD-CODED RELEASE VERSIONS in voice-bridge.py:"
|
||||
echo "$hits"
|
||||
exit 1
|
||||
fi
|
||||
'
|
||||
|
||||
# ── 5. Walk-up behavior: resolver finds VERSION in a parent directory ─────
|
||||
# Behavioral test: copy voice-bridge.py into a temp tree nested several
|
||||
# directories deep, place a synthetic VERSION at the temp root, and verify
|
||||
# the resolver walks up and returns the synthetic value.
|
||||
assert "voice-bridge.py: walk-up resolver finds VERSION in parent directory" \
|
||||
bash -c '
|
||||
nested_dir="$TEST_TMPDIR/walkup/nested/deep"
|
||||
mkdir -p "$nested_dir"
|
||||
cp "'"$VOICE_BRIDGE"'" "$nested_dir/voice-bridge.py"
|
||||
echo "9.9.9-walkup" > "$TEST_TMPDIR/walkup/VERSION"
|
||||
cd "$nested_dir" && \
|
||||
actual="$(python3 -c "import importlib.util; spec = importlib.util.spec_from_file_location(\"voice_bridge\", \"voice-bridge.py\"); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); print(m.VERSION)")"
|
||||
if [ "$actual" = "9.9.9-walkup" ]; then
|
||||
exit 0
|
||||
fi
|
||||
echo "walk-up expected=9.9.9-walkup actual=$actual"
|
||||
exit 1
|
||||
'
|
||||
|
||||
# ── 5b. Path-construction shape check ─────────────────────────────────────
|
||||
# The resolver MUST build a candidate path using either string concat
|
||||
# ("/VERSION") or os.path.join(... "VERSION"). This is a structural
|
||||
# check — the behavioral version is test 5.
|
||||
assert "voice-bridge.py: resolver constructs VERSION candidate path" \
|
||||
bash -c '
|
||||
grep -qE "(os\\.path\\.join\\([^)]*VERSION|\"/VERSION\"|os\\.path\\.join\\(d, .VERSION.\\))" "'"$VOICE_BRIDGE"'"
|
||||
'
|
||||
|
||||
# ── 6. Runtime: VERSION constant matches repo VERSION ─────────────────────
|
||||
assert "voice-bridge.py: VERSION constant resolves from repo root" \
|
||||
bash -c '
|
||||
cd "'"$REPO_ROOT"'" && \
|
||||
expected="$(head -n1 VERSION)" && \
|
||||
actual="$(cd "'"$(dirname "$VOICE_BRIDGE")"'" && python3 -c "import importlib.util; spec = importlib.util.spec_from_file_location(\"voice_bridge\", \"voice-bridge.py\"); m = importlib.util.module_from_spec(spec); spec.loader.exec_module(m); print(m.VERSION)")" && \
|
||||
[ "$actual" = "$expected" ] || { echo "expected=$expected actual=$actual"; exit 1; }
|
||||
'
|
||||
|
||||
# ── 7. /etc/samios-version fallback: BEHAVIORAL with call-flag ────────────
|
||||
# Instrument the fake open() with a flag that proves it was called. Load
|
||||
# the module from a temp tree where no ancestor VERSION exists so the
|
||||
# walk-up falls through to /etc/samios-version. Assert both that the
|
||||
# fallback was reached AND that the synthesized value was returned.
|
||||
assert "voice-bridge.py: /etc/samios-version fallback returns synthesized value" \
|
||||
bash -c '
|
||||
isolated_dir="$TEST_TMPDIR/fallback/iso/nested"
|
||||
mkdir -p "$isolated_dir" || exit 1
|
||||
cp "'"$VOICE_BRIDGE"'" "$isolated_dir/voice-bridge.py"
|
||||
cd "$isolated_dir" || exit 1
|
||||
python3 << "PYEOF"
|
||||
import importlib.util, builtins, os
|
||||
spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
|
||||
SYNTH = "9.9.9-etc-synth"
|
||||
|
||||
etc_called = [False]
|
||||
real_open = builtins.open
|
||||
real_isfile = os.path.isfile
|
||||
|
||||
class FakeEtcFile:
|
||||
def __enter__(self): return self
|
||||
def __exit__(self, *a): pass
|
||||
def read(self): return SYNTH + chr(10)
|
||||
def readline(self): return SYNTH + chr(10)
|
||||
|
||||
def fake_open(path, *args, **kwargs):
|
||||
if isinstance(path, str) and path == "/etc/samios-version":
|
||||
etc_called[0] = True
|
||||
return FakeEtcFile()
|
||||
return real_open(path, *args, **kwargs)
|
||||
|
||||
# Suppress ALL ancestor VERSION candidates so the resolver falls through
|
||||
# to /etc/samios-version. Without this, a VERSION file anywhere on the
|
||||
# walk-up path (including SAMIOS_TEST_TMPDIR=/) would short-circuit the
|
||||
# test.
|
||||
builtins.open = fake_open
|
||||
os.path.isfile = lambda p: False
|
||||
|
||||
try:
|
||||
spec.loader.exec_module(m)
|
||||
finally:
|
||||
builtins.open = real_open
|
||||
os.path.isfile = real_isfile
|
||||
|
||||
assert etc_called[0], "fake /etc open was never called -- fallback not reached"
|
||||
assert m.VERSION == SYNTH, f"fallback returned {m.VERSION!r}, expected {SYNTH!r}"
|
||||
print("OK", m.VERSION)
|
||||
PYEOF
|
||||
'
|
||||
|
||||
# ── 8. Placeholder fallback: hermetic, asserts exactly 0.0.0-unknown ──────
|
||||
# Suppress ALL candidate VERSION sources (walk-up + /etc) and assert the
|
||||
# resolver returns EXACTLY the documented placeholder. This is the strongest
|
||||
# fallback test — it isolates the failure mode.
|
||||
assert "voice-bridge.py: placeholder fallback returns exactly '0.0.0-unknown'" \
|
||||
bash -c '
|
||||
isolated_dir="$TEST_TMPDIR/placeholder/iso/nested"
|
||||
mkdir -p "$isolated_dir"
|
||||
cp "'"$VOICE_BRIDGE"'" "$isolated_dir/voice-bridge.py"
|
||||
cd "$isolated_dir" || exit 1
|
||||
python3 << "PYEOF"
|
||||
import importlib.util, builtins, os, sys
|
||||
spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
|
||||
def always_fail(path, *args, **kwargs):
|
||||
raise OSError("forced failure for hermetic test")
|
||||
|
||||
real_open = builtins.open
|
||||
real_isfile = os.path.isfile
|
||||
builtins.open = always_fail
|
||||
os.path.isfile = lambda p: False
|
||||
|
||||
try:
|
||||
spec.loader.exec_module(m)
|
||||
finally:
|
||||
builtins.open = real_open
|
||||
os.path.isfile = real_isfile
|
||||
|
||||
assert m.VERSION == "0.0.0-unknown", f"placeholder fallback returned {m.VERSION!r}"
|
||||
print("OK", m.VERSION)
|
||||
PYEOF
|
||||
'
|
||||
|
||||
# ── 9. VoiceBridge class: behavioral smoke test ───────────────────────────
|
||||
# Construct a VoiceBridge, exercise its public attributes and methods
|
||||
# (handle_transcript, _handle_dictation, _enter_dictation, _exit_dictation,
|
||||
# mode transitions) with monkeypatched xdotool/hermes — no source grep.
|
||||
assert "voice-bridge.py: VoiceBridge is constructible and starts in IDLE mode" \
|
||||
bash -c '
|
||||
cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1
|
||||
python3 << "PYEOF"
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
bridge = m.VoiceBridge()
|
||||
assert bridge.mode == m.VoiceMode.IDLE, f"expected IDLE, got {bridge.mode}"
|
||||
assert bridge.running is False
|
||||
assert hasattr(bridge, "dictation_commands")
|
||||
assert "stop dictation" in bridge.dictation_commands
|
||||
assert "new paragraph" in bridge.dictation_commands
|
||||
print("OK")
|
||||
PYEOF
|
||||
'
|
||||
|
||||
assert "voice-bridge.py: 'start dictation' command enters DICTATION mode" \
|
||||
bash -c '
|
||||
cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1
|
||||
python3 << "PYEOF"
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
bridge = m.VoiceBridge()
|
||||
assert bridge.mode == m.VoiceMode.IDLE
|
||||
bridge.handle_transcript({"text": "start dictation"})
|
||||
assert bridge.mode == m.VoiceMode.DICTATION, f"expected DICTATION after wake, got {bridge.mode}"
|
||||
bridge.handle_transcript({"text": "stop dictation"})
|
||||
assert bridge.mode == m.VoiceMode.IDLE, f"expected IDLE after stop, got {bridge.mode}"
|
||||
print("OK")
|
||||
PYEOF
|
||||
'
|
||||
|
||||
assert "voice-bridge.py: 'computer' wake word strips prefix from command" \
|
||||
bash -c '
|
||||
cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1
|
||||
python3 << "PYEOF"
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
bridge = m.VoiceBridge()
|
||||
captured = []
|
||||
bridge._handle_command = lambda cmd, conf: captured.append((cmd, conf))
|
||||
bridge._send_to_hermes = lambda cmd: None
|
||||
bridge.handle_transcript({"text": "computer open firefox", "confidence": 1.0})
|
||||
assert captured == [("open firefox", 1.0)], f"expected [(open firefox, 1.0)], got {captured}"
|
||||
print("OK")
|
||||
PYEOF
|
||||
'
|
||||
|
||||
assert "voice-bridge.py: 'computer' wake word is case-insensitive" \
|
||||
bash -c '
|
||||
cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1
|
||||
python3 << "PYEOF"
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
bridge = m.VoiceBridge()
|
||||
captured = []
|
||||
bridge._handle_command = lambda cmd, conf: captured.append((cmd, conf))
|
||||
bridge._send_to_hermes = lambda cmd: None
|
||||
# Wake word is matched via text.lower().startswith("computer"), so
|
||||
# uppercase/mixed-case "COMPUTER" must also trigger.
|
||||
bridge.handle_transcript({"text": "COMPUTER shutdown", "confidence": 1.0})
|
||||
assert captured == [("shutdown", 1.0)], f"expected [(shutdown, 1.0)], got {captured}"
|
||||
print("OK")
|
||||
PYEOF
|
||||
'
|
||||
|
||||
assert "voice-bridge.py: dictation mode routes known phrases to key actions" \
|
||||
bash -c '
|
||||
cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1
|
||||
python3 << "PYEOF"
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
bridge = m.VoiceBridge()
|
||||
sent_keys = []
|
||||
bridge._send_key = lambda k: sent_keys.append(k)
|
||||
bridge._enter_dictation()
|
||||
assert bridge.mode == m.VoiceMode.DICTATION
|
||||
bridge.handle_transcript({"text": "new paragraph", "confidence": 1.0})
|
||||
bridge.handle_transcript({"text": "tab", "confidence": 1.0})
|
||||
assert sent_keys == ["Return", "Tab"], f"expected [Return, Tab], got {sent_keys}"
|
||||
print("OK")
|
||||
PYEOF
|
||||
'
|
||||
|
||||
# ── 10. main() entry point is wired correctly ─────────────────────────────
|
||||
assert "voice-bridge.py: main() entry point exists and exits 0 on --version" \
|
||||
bash -c '
|
||||
cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1
|
||||
python3 << "PYEOF"
|
||||
import importlib.util, sys
|
||||
spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
assert callable(m.main)
|
||||
saved_argv = sys.argv
|
||||
try:
|
||||
sys.argv = ["voice-bridge.py", "--version"]
|
||||
try:
|
||||
m.main()
|
||||
sys.exit(2)
|
||||
except SystemExit as e:
|
||||
if e.code != 0:
|
||||
print(f"main() exited with {e.code}")
|
||||
sys.exit(1)
|
||||
print("OK")
|
||||
finally:
|
||||
sys.argv = saved_argv
|
||||
PYEOF
|
||||
'
|
||||
|
||||
# ── 11. dictation_commands dict is reachable and contains expected keys ───
|
||||
assert "voice-bridge.py: dictation_commands dict has at least 10 entries" \
|
||||
bash -c '
|
||||
cd "'"$(dirname "$VOICE_BRIDGE")"'" || exit 1
|
||||
python3 << "PYEOF"
|
||||
import importlib.util
|
||||
spec = importlib.util.spec_from_file_location("voice_bridge", "voice-bridge.py")
|
||||
m = importlib.util.module_from_spec(spec)
|
||||
spec.loader.exec_module(m)
|
||||
bridge = m.VoiceBridge()
|
||||
n = len(bridge.dictation_commands)
|
||||
assert n >= 10, f"expected at least 10 dictation commands, got {n}"
|
||||
print("OK", n)
|
||||
PYEOF
|
||||
'
|
||||
|
||||
print_summary
|
||||
Reference in New Issue
Block a user