Skip to content

Commit 135b1e8

Browse files
authored
Merge pull request #2 from csheaff/dev
CI improvements and comprehensive test suite
2 parents ba84745 + cfdc54b commit 135b1e8

File tree

7 files changed

+211
-7
lines changed

7 files changed

+211
-7
lines changed

.github/workflows/ci.yml

Lines changed: 3 additions & 5 deletions
Original file line numberDiff line numberDiff line change
@@ -1,8 +1,6 @@
11
name: CI
22

33
on:
4-
push:
5-
branches: [main]
64
pull_request:
75
branches: [main]
86

@@ -32,7 +30,7 @@ jobs:
3230
run: sudo apt-get update && sudo apt-get install -y bats
3331

3432
- name: Install test dependencies
35-
run: sudo apt-get install -y ydotool pipewire libnotify-bin
33+
run: sudo apt-get install -y ydotool pipewire libnotify-bin socat
3634

37-
- name: Run unit tests
38-
run: bats test/talktype.bats
35+
- name: Run tests
36+
run: bats test/talktype.bats test/server.bats

test/mock-daemon.py

Lines changed: 36 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,36 @@
1+
"""Mock transcription daemon for testing server mode."""
2+
import os
3+
import sys
4+
import socket
5+
import signal
6+
7+
SOCK_PATH = sys.argv[1]
8+
9+
def cleanup(*_):
10+
try:
11+
os.unlink(SOCK_PATH)
12+
except OSError:
13+
pass
14+
sys.exit(0)
15+
16+
signal.signal(signal.SIGTERM, cleanup)
17+
signal.signal(signal.SIGINT, cleanup)
18+
19+
if os.path.exists(SOCK_PATH):
20+
os.unlink(SOCK_PATH)
21+
22+
server = socket.socket(socket.AF_UNIX, socket.SOCK_STREAM)
23+
server.bind(SOCK_PATH)
24+
server.listen(1)
25+
26+
print("Mock daemon ready.", flush=True)
27+
28+
while True:
29+
conn, _ = server.accept()
30+
try:
31+
audio_path = conn.recv(4096).decode().strip()
32+
conn.sendall(b"mock transcription result")
33+
except Exception:
34+
conn.sendall(b"")
35+
finally:
36+
conn.close()

test/mock-transcribe-fail

Lines changed: 4 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,4 @@
1+
#!/usr/bin/env bash
2+
# Mock transcriber that fails
3+
echo "error: model not found" >&2
4+
exit 1

test/mocks/ffmpeg

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env bash
2-
# Mock ffmpeg: sleep like a recording process
2+
# Mock ffmpeg: log that it was called, sleep like a recording process
3+
echo "ffmpeg" > "$TALKTYPE_DIR/recorder.log"
34
trap 'exit 0' TERM
45
sleep 300 &
56
wait $!

test/mocks/pw-record

Lines changed: 2 additions & 1 deletion
Original file line numberDiff line numberDiff line change
@@ -1,5 +1,6 @@
11
#!/usr/bin/env bash
2-
# Mock pw-record: write PID so tests can clean up, sleep in background
2+
# Mock pw-record: log that it was called, sleep like a recording process
3+
echo "pw-record" > "$TALKTYPE_DIR/recorder.log"
34
trap 'exit 0' TERM
45
sleep 300 &
56
wait $!

test/server.bats

Lines changed: 99 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,99 @@
1+
#!/usr/bin/env bats
2+
3+
# Tests for the server mode (Unix socket daemon pattern).
4+
# Uses a mock daemon so no real models or venvs are needed.
5+
6+
REPO_DIR="$BATS_TEST_DIRNAME/.."
7+
8+
setup() {
9+
export SOCK="$BATS_TEST_TMPDIR/test-server.sock"
10+
export PIDFILE="$BATS_TEST_TMPDIR/test-server.pid"
11+
}
12+
13+
teardown() {
14+
# Clean up any leftover daemon
15+
if [ -f "$PIDFILE" ]; then
16+
kill "$(cat "$PIDFILE")" 2>/dev/null || true
17+
rm -f "$PIDFILE"
18+
fi
19+
rm -f "$SOCK"
20+
}
21+
22+
# Helper: start the mock daemon
23+
start_mock_daemon() {
24+
python3 "$BATS_TEST_DIRNAME/mock-daemon.py" "$SOCK" &
25+
local pid=$!
26+
echo "$pid" > "$PIDFILE"
27+
28+
# Wait for socket to appear
29+
for i in $(seq 1 10); do
30+
[ -S "$SOCK" ] && return 0
31+
sleep 0.1
32+
done
33+
return 1
34+
}
35+
36+
# ── Daemon lifecycle ──
37+
38+
@test "mock daemon starts and creates socket" {
39+
start_mock_daemon
40+
41+
[ -S "$SOCK" ]
42+
[ -f "$PIDFILE" ]
43+
kill -0 "$(cat "$PIDFILE")"
44+
}
45+
46+
@test "mock daemon responds to transcription requests" {
47+
command -v socat &>/dev/null || skip "socat not installed"
48+
start_mock_daemon
49+
50+
result=$(echo "/tmp/test.wav" | socat - UNIX-CONNECT:"$SOCK")
51+
[[ "$result" == "mock transcription result" ]]
52+
}
53+
54+
@test "mock daemon handles multiple requests" {
55+
command -v socat &>/dev/null || skip "socat not installed"
56+
start_mock_daemon
57+
58+
result1=$(echo "/tmp/a.wav" | socat - UNIX-CONNECT:"$SOCK")
59+
result2=$(echo "/tmp/b.wav" | socat - UNIX-CONNECT:"$SOCK")
60+
[[ "$result1" == "mock transcription result" ]]
61+
[[ "$result2" == "mock transcription result" ]]
62+
}
63+
64+
@test "daemon cleans up socket on SIGTERM" {
65+
start_mock_daemon
66+
67+
[ -S "$SOCK" ]
68+
kill "$(cat "$PIDFILE")"
69+
sleep 0.2
70+
71+
[ ! -S "$SOCK" ]
72+
}
73+
74+
# ── Server wrapper logic ──
75+
76+
@test "transcribe fails with helpful message when server not running" {
77+
# Test each server script's transcribe command without a running server
78+
for server in transcribe-server backends/parakeet-server backends/moonshine-server; do
79+
run "$REPO_DIR/$server" transcribe /tmp/test.wav
80+
[ "$status" -eq 1 ]
81+
[[ "$output" == *"not running"* ]]
82+
done
83+
}
84+
85+
@test "stop reports not running when no pidfile exists" {
86+
for server in transcribe-server backends/parakeet-server backends/moonshine-server; do
87+
run "$REPO_DIR/$server" stop
88+
[ "$status" -eq 0 ]
89+
[[ "$output" == *"Not running"* ]]
90+
done
91+
}
92+
93+
@test "invalid subcommand shows usage" {
94+
for server in transcribe-server backends/parakeet-server backends/moonshine-server; do
95+
run "$REPO_DIR/$server" invalid
96+
[ "$status" -eq 1 ]
97+
[[ "$output" == *"Usage"* ]]
98+
done
99+
}

test/talktype.bats

Lines changed: 65 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -95,6 +95,71 @@ start_fake_recording() {
9595
[ ! -f "$TALKTYPE_DIR/ydotool.log" ]
9696
}
9797

98+
# ── Error handling ──
99+
100+
@test "stale pid file with dead process still transcribes" {
101+
# Simulate a crashed recording: PID file points to a dead process
102+
echo "99999" > "$TALKTYPE_DIR/rec.pid"
103+
echo "audio data" > "$TALKTYPE_DIR/rec.wav"
104+
105+
run "$TALKTYPE"
106+
[ "$status" -eq 0 ]
107+
108+
# Should have cleaned up and transcribed
109+
[ ! -f "$TALKTYPE_DIR/rec.pid" ]
110+
[[ "$(cat "$TALKTYPE_DIR/ydotool.log")" == *"hello world"* ]]
111+
}
112+
113+
@test "transcription command failure is handled" {
114+
start_fake_recording
115+
export TALKTYPE_CMD="$BATS_TEST_DIRNAME/mock-transcribe-fail"
116+
117+
run "$TALKTYPE"
118+
119+
# Script should fail (set -e catches the non-zero exit)
120+
[ "$status" -ne 0 ]
121+
122+
# ydotool should NOT have been called
123+
[ ! -f "$TALKTYPE_DIR/ydotool.log" ]
124+
}
125+
126+
# ── Recorder selection ──
127+
128+
@test "ffmpeg is preferred over pw-record when available" {
129+
run "$TALKTYPE"
130+
[ "$status" -eq 0 ]
131+
[ -f "$TALKTYPE_DIR/recorder.log" ]
132+
133+
[[ "$(cat "$TALKTYPE_DIR/recorder.log")" == "ffmpeg" ]]
134+
}
135+
136+
@test "pw-record is used when ffmpeg is not available" {
137+
# Remove ffmpeg from PATH by creating a sparse PATH without it
138+
local sparse="$BATS_TEST_TMPDIR/no_ffmpeg"
139+
mkdir -p "$sparse"
140+
141+
# Copy all mocks except ffmpeg
142+
for mock in "$BATS_TEST_DIRNAME"/mocks/*; do
143+
name=$(basename "$mock")
144+
[ "$name" = "ffmpeg" ] && continue
145+
ln -sf "$mock" "$sparse/$name"
146+
done
147+
148+
# Add essential system tools
149+
for cmd in bash mkdir cat kill sleep echo rm wait; do
150+
local path
151+
path=$(command -v "$cmd" 2>/dev/null) && ln -sf "$path" "$sparse/$cmd"
152+
done
153+
154+
PATH="$sparse"
155+
156+
run "$TALKTYPE"
157+
[ "$status" -eq 0 ]
158+
[ -f "$TALKTYPE_DIR/recorder.log" ]
159+
160+
[[ "$(cat "$TALKTYPE_DIR/recorder.log")" == "pw-record" ]]
161+
}
162+
98163
# ── Dependency checking ──
99164

100165
@test "fails when a required tool is missing" {

0 commit comments

Comments
 (0)