feat(remote-control): add screen/screenshot/tap/swipe/type/key/launch/ocr/sms commands

- lib/input.sh: ADB input automation library (screenshot, tap, swipe, type, key, launch, OCR, SMS/OTP via KDE Connect)
- fs5: 9 new remote control commands for AI agent capabilities
- tests/test_remote_control.bats: 31 tests, all passing
- specs/002-remote-control/spec.md: feature specification

Tools: scrcpy 3.3.4 (GUI mirror), tesseract-ocr (OCR), KDE Connect (SMS/OTP)
Constraints: no root required, Android 9 (SDK 28)

Tests: 64/64 passing (33 device mgmt + 31 remote control)
This commit is contained in:
Jane Alesi
2026-03-02 16:10:11 +01:00
parent e6e5f34455
commit aaf008c1bf
4 changed files with 927 additions and 7 deletions
+237
View File
@@ -0,0 +1,237 @@
#!/usr/bin/env bats
# tests/test_remote_control.bats — Remote control command tests for fs5
# Tests: screenshot, tap, swipe, type, key, launch, ocr, sms
setup() {
SCRIPT_DIR="$(cd "$(dirname "${BATS_TEST_FILENAME}")/.." && pwd)"
FS5="${SCRIPT_DIR}/fs5"
DEVICE_SERIAL="${DEVICE_SERIAL:-RD51QE202392}"
export DEVICE_SERIAL
}
teardown() {
# Clean up any test screenshots
rm -f /tmp/fs5-test-screenshot-*.png
}
# ── screenshot ────────────────────────────────────────────────────────────────
@test "screenshot: saves PNG to default path" {
run "${FS5}" screenshot
[ "${status}" -eq 0 ]
# Output contains a path
echo "${output}" | grep -q "/tmp/fs5-screenshot-"
# File exists
local path
path=$(echo "${output}" | grep "/tmp/fs5-screenshot-" | tail -1)
[ -f "${path}" ]
rm -f "${path}"
}
@test "screenshot: saves PNG to specified path" {
local out="/tmp/fs5-test-screenshot-$$.png"
run "${FS5}" screenshot "${out}"
[ "${status}" -eq 0 ]
[ -f "${out}" ]
}
@test "screenshot: --json output contains path and timestamp" {
run "${FS5}" screenshot --json
[ "${status}" -eq 0 ]
echo "${output}" | python3 -c "
import sys, json
data = json.load(sys.stdin)
assert 'path' in data, 'missing path'
assert 'timestamp' in data, 'missing timestamp'
assert data['path'].endswith('.png'), 'path not PNG'
"
# Clean up
local path
path=$(echo "${output}" | python3 -c "import sys,json; print(json.load(sys.stdin)['path'])")
rm -f "${path}"
}
@test "screenshot: --help shows usage" {
run "${FS5}" screenshot --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -qi "usage"
}
# ── tap ───────────────────────────────────────────────────────────────────────
@test "tap: sends tap at valid coordinates" {
run "${FS5}" tap 540 960
[ "${status}" -eq 0 ]
echo "${output}" | grep -q "Tapped"
}
@test "tap: fails without coordinates" {
run "${FS5}" tap
[ "${status}" -ne 0 ]
echo "${output}" | grep -qi "usage"
}
@test "tap: fails with only one coordinate" {
run "${FS5}" tap 540
[ "${status}" -ne 0 ]
}
@test "tap: --help shows usage" {
run "${FS5}" tap --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -qi "usage"
}
# ── swipe ─────────────────────────────────────────────────────────────────────
@test "swipe: performs swipe with four coordinates" {
run "${FS5}" swipe 540 1200 540 400
[ "${status}" -eq 0 ]
echo "${output}" | grep -q "Swiped"
}
@test "swipe: performs swipe with custom duration" {
run "${FS5}" swipe 540 1200 540 400 500
[ "${status}" -eq 0 ]
echo "${output}" | grep -q "500ms"
}
@test "swipe: fails without coordinates" {
run "${FS5}" swipe
[ "${status}" -ne 0 ]
}
@test "swipe: --help shows usage" {
run "${FS5}" swipe --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -qi "usage"
}
# ── key ───────────────────────────────────────────────────────────────────────
@test "key: sends KEYCODE_HOME" {
run "${FS5}" key KEYCODE_HOME
[ "${status}" -eq 0 ]
echo "${output}" | grep -q "Key sent"
}
@test "key: sends numeric keycode" {
run "${FS5}" key 3
[ "${status}" -eq 0 ]
echo "${output}" | grep -q "Key sent"
}
@test "key: fails without keycode" {
run "${FS5}" key
[ "${status}" -ne 0 ]
}
@test "key: --help shows usage (global help intercepts --help flag)" {
run "${FS5}" key --help
[ "${status}" -eq 0 ]
# --help is a global flag; shows main usage with key command listed
echo "${output}" | grep -q "key"
}
# ── launch ────────────────────────────────────────────────────────────────────
@test "launch: launches KDE Connect app" {
run "${FS5}" launch org.kde.kdeconnect_tp
[ "${status}" -eq 0 ]
echo "${output}" | grep -q "Launched"
}
@test "launch: fails for non-existent package" {
run "${FS5}" launch com.nonexistent.package.xyz
[ "${status}" -ne 0 ]
}
@test "launch: fails without package name" {
run "${FS5}" launch
[ "${status}" -ne 0 ]
}
@test "launch: --help shows usage" {
run "${FS5}" launch --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -qi "usage"
}
# ── ocr ───────────────────────────────────────────────────────────────────────
@test "ocr: runs without error and returns text or empty" {
run "${FS5}" ocr
[ "${status}" -eq 0 ]
# Output may be empty (blank screen) or contain text — both valid
}
@test "ocr: --help shows usage" {
run "${FS5}" ocr --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -qi "usage\|screenshot\|ocr"
}
# ── sms ───────────────────────────────────────────────────────────────────────
@test "sms: no subcommand shows error" {
run "${FS5}" sms
[ "${status}" -ne 0 ]
echo "${output}" | grep -qi "unknown\|subcommand\|use:"
}
@test "sms: --help shows subcommands" {
run "${FS5}" sms --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -q "list"
echo "${output}" | grep -q "otp"
echo "${output}" | grep -q "watch"
}
@test "sms list: shows KDE Connect setup instructions when not paired" {
run "${FS5}" sms list
# May succeed (if paired) or fail with setup instructions
if [ "${status}" -ne 0 ]; then
echo "${output}" | grep -qi "kde connect\|paired\|setup"
fi
}
@test "sms otp: shows error when KDE Connect not paired" {
run "${FS5}" sms otp
# Either returns OTP (if paired) or error about KDE Connect
if [ "${status}" -ne 0 ]; then
echo "${output}" | grep -qi "kde connect\|paired\|otp"
fi
}
@test "sms otp: --help shows usage" {
run "${FS5}" sms otp --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -qi "usage\|otp\|wait"
}
@test "sms list: --help shows usage" {
run "${FS5}" sms list --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -qi "usage"
}
# ── screen ────────────────────────────────────────────────────────────────────
@test "screen: --help shows usage" {
run "${FS5}" screen --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -qi "scrcpy\|mirror\|headless"
}
# ── type ──────────────────────────────────────────────────────────────────────
@test "type: --help shows usage" {
run "${FS5}" type --help
[ "${status}" -eq 0 ]
echo "${output}" | grep -qi "usage"
}
@test "type: fails without text argument" {
run "${FS5}" type
[ "${status}" -ne 0 ]
}