Files
voculinux/tests/test_command_processor.py
T
94f71853e0 reliability: Microphone Reconnection and Audio Buffer Limits (#109)
* fix: implement microphone reconnection and audio buffer limits for reliability

- Add microphone reconnection logic with exponential backoff
- Implement audio buffer limits to prevent memory issues
- Add configurable buffer size limits with clamping
- Improve error handling for audio device failures
- Add buffer statistics and monitoring capabilities
- Graceful stream cleanup and resource management

Resolves #92

Changes made:
- Added _max_buffer_size, _reconnection_attempts, _max_reconnection_attempts attributes
- Implemented _attempt_audio_reconnection() method with exponential backoff
- Enhanced _record_audio() method with buffer limit checking and reconnection
- Added set_buffer_limit() and get_buffer_stats() methods
- Improved error handling for IOError/OSError during audio recording
- Added test scripts to validate reliability improvements

The implementation prevents memory issues by limiting audio buffer size
and automatically reconnecting to audio devices when they fail,
with proper exponential backoff to prevent rapid reconnection attempts.

* fix: Address code review feedback for PR #109

- Add thread lock (_buffer_lock) for audio_buffer to prevent race conditions
- Fix _last_audio_error_time reset in cleanup
- Protect buffer access in stop_recognition and get_buffer_stats with lock
- Fix test_max_reconnection_attempts to match actual counter behavior

Addresses review feedback for microphone reconnection and audio buffer limits

* style: fix lint errors

* fix: add missing sys import in audio_feedback.py

- Add sys import required by _is_ci_mode() function
- Remove temporary test_reliability scripts

* style: fix black formatting

---------

Co-authored-by: OpenClaw Reviewer <review@openclaw.ai>
2026-02-04 22:52:39 -08:00

344 lines
15 KiB
Python

"""
Tests for the command processor functionality.
"""
import unittest
# Update import path to use the new package structure
from vocalinux.speech_recognition.command_processor import CommandProcessor
class TestCommandProcessor(unittest.TestCase):
"""Test cases for command processor functionality."""
def setUp(self):
"""Set up for tests."""
self.processor = CommandProcessor()
def test_initialization(self):
"""Test initialization of command processor."""
# Verify command dictionaries are initialized
self.assertTrue(hasattr(self.processor, "text_commands"))
self.assertTrue(hasattr(self.processor, "action_commands"))
self.assertTrue(hasattr(self.processor, "format_commands"))
# Verify dictionaries have expected entries
self.assertIn("new line", self.processor.text_commands)
self.assertIn("period", self.processor.text_commands)
self.assertIn("delete that", self.processor.action_commands)
self.assertIn("capitalize", self.processor.format_commands)
# Verify regex patterns are compiled
self.assertTrue(hasattr(self.processor, "text_cmd_regex"))
self.assertTrue(hasattr(self.processor, "action_cmd_regex"))
self.assertTrue(hasattr(self.processor, "format_cmd_regex"))
def test_text_command_processing(self):
"""Test processing of text commands."""
# Test common text commands
test_cases = [
("new line", "\n", []),
("this is a new paragraph", "this is a \n\n", []),
("end of sentence period", "end of sentence.", []),
("add a comma here", "add a, here", []),
("use question mark", "use?", []),
("exclamation mark test", "! test", []),
("semicolon example", "; example", []),
("testing colon usage", "testing:", []),
("dash separator", "- separator", []),
("hyphen example", "- example", []),
("underscore value", "_ value", []),
("quote example", '" example', []),
("single quote test", "' test", []),
("open parenthesis content close parenthesis", "( content)", []),
("open bracket item close bracket", "[ item]", []),
("open brace code close brace", "{ code}", []),
]
for input_text, expected_output, _ in test_cases:
result, actions = self.processor.process_text(input_text)
self.assertEqual(result, expected_output)
self.assertEqual(actions, [])
def test_action_command_processing(self):
"""Test processing of action commands."""
# Test action commands
test_cases = [
("delete that", "", ["delete_last"]),
("scratch that previous text", " previous text", ["delete_last"]),
("undo my last change", " my last change", ["undo"]),
("redo that edit", " that edit", ["redo"]),
("select all text", " text", ["select_all"]),
("select line of code", " of code", ["select_line"]),
("select word here", " here", ["select_word"]),
("select paragraph content", " content", ["select_paragraph"]),
("cut this selection", " this selection", ["cut"]),
("copy this text", " this text", ["copy"]),
("paste here", " here", ["paste"]),
# Test multiple actions
("select all then copy", " then", ["select_all", "copy"]),
]
for input_text, expected_output, expected_actions in test_cases:
result, actions = self.processor.process_text(input_text)
self.assertEqual(result, expected_output)
self.assertEqual(actions, expected_actions)
def test_format_command_processing(self):
"""Test processing of formatting commands."""
# Test format commands
test_cases = [
("capitalize word", "Word", []),
("uppercase letters", "LETTERS", []),
("all caps example", "EXAMPLE", []),
("lowercase TEXT", "text", []),
# Test with text before the command
("make this capitalize next", "make this Next", []),
]
for input_text, expected_output, expected_actions in test_cases:
result, actions = self.processor.process_text(input_text)
self.assertEqual(result, expected_output)
self.assertEqual(actions, expected_actions)
def test_combined_commands(self):
"""Test combinations of different command types."""
test_cases = [
# Text + Action
("new line then delete that", "", ["delete_last"]),
# Format + Text
("capitalize name period", "Name.", []),
# Action + Format
("select all then capitalize text", " then Text", ["select_all"]),
# Complex combination
(
"capitalize name comma new line select paragraph",
"Name,\n",
["select_paragraph"],
),
]
for input_text, expected_output, expected_actions in test_cases:
result, actions = self.processor.process_text(input_text)
self.assertEqual(result, expected_output)
self.assertEqual(actions, expected_actions)
def test_empty_input(self):
"""Test handling of empty input."""
result, actions = self.processor.process_text("")
self.assertEqual(result, "")
self.assertEqual(actions, [])
result, actions = self.processor.process_text(None)
self.assertEqual(result, "")
self.assertEqual(actions, [])
def test_no_commands(self):
"""Test text with no commands."""
input_text = "This is just regular text with no commands."
result, actions = self.processor.process_text(input_text)
self.assertEqual(result, input_text)
self.assertEqual(actions, [])
def test_partial_command_matches(self):
"""Test text with partial command matches."""
# Words that are substrings of commands should not be processed
test_cases = [
("periodic review", "periodic review", []), # shouldn't match "period"
("newcomer", "newcomer", []), # shouldn't match "new"
("paramount", "paramount", []), # shouldn't match "paragraph"
]
for input_text, expected_output, expected_actions in test_cases:
result, actions = self.processor.process_text(input_text)
self.assertEqual(result, expected_output)
self.assertEqual(actions, expected_actions)
def test_case_insensitivity(self):
"""Test that command matching is case-insensitive."""
test_cases = [
("NEW LINE", "\n", []),
("Period", ".", []),
("DELETE THAT", "", ["delete_last"]),
("Capitalize word", "Word", []),
]
for input_text, expected_output, expected_actions in test_cases:
result, actions = self.processor.process_text(input_text)
self.assertEqual(result, expected_output)
self.assertEqual(actions, expected_actions)
def test_multiple_format_modifiers(self):
"""Test multiple format modifiers on a single word."""
# Reset command processor to ensure active_formats is empty
self.processor = CommandProcessor()
# Apply multiple formatting commands
result, _ = self.processor.process_text("capitalize all caps text")
# The second format command overrides the first, resulting in all uppercase
self.assertEqual(result, "TEXT")
def test_format_with_no_target_word(self):
"""Test format command with no following word."""
result, _ = self.processor.process_text("capitalize")
self.assertEqual(result, "")
# The active_formats should be cleared even if no word was formatted
self.assertEqual(self.processor.active_formats, set())
def test_whitespace_handling(self):
"""Test handling of whitespace in command processing."""
# Test with extra spaces
result, _ = self.processor.process_text("new line test")
self.assertEqual(result, "\n test")
# Test with leading/trailing spaces
result, _ = self.processor.process_text(" period ")
self.assertEqual(result, ".")
# Test with mixed whitespace
result, _ = self.processor.process_text(" capitalize word new line ")
self.assertEqual(result, "Word \n")
def test_regex_compilation(self):
"""Test the regex pattern compilation."""
self.processor._compile_patterns()
# Test regex patterns match correctly
self.assertTrue(self.processor.text_cmd_regex.search("new line"))
self.assertTrue(self.processor.text_cmd_regex.search("this is a period"))
self.assertTrue(self.processor.action_cmd_regex.search("delete that"))
self.assertTrue(self.processor.format_cmd_regex.search("capitalize this"))
# Test non-matches
self.assertFalse(self.processor.text_cmd_regex.search("newline")) # no space
self.assertFalse(self.processor.action_cmd_regex.search("deletion")) # not a command
def test_compile_patterns_method(self):
"""Test the _compile_patterns method directly."""
# Add a new command to test recompilation
self.processor.text_commands["test command"] = "TEST"
# Recompile patterns
self.processor._compile_patterns()
# Verify new command is in the pattern
self.assertTrue(self.processor.text_cmd_regex.search("test command"))
class TestCommandProcessorFallback(unittest.TestCase):
"""Test the generic fallback processing logic in CommandProcessor."""
def setUp(self):
"""Set up for tests."""
self.processor = CommandProcessor()
def test_generic_action_command_with_following_text(self):
"""Test action command followed by text through generic path."""
# Use text that won't match hardcoded test cases
result, actions = self.processor.process_text("undo the error")
# Should trigger action and capture following text
self.assertIn("undo", actions)
def test_generic_action_command_alone(self):
"""Test action command alone through generic path."""
# "redo" alone isn't hardcoded, so goes through generic
result, actions = self.processor.process_text("redo only")
self.assertIn("redo", actions)
def test_generic_text_command_punctuation(self):
"""Test punctuation text commands through generic path."""
# "full stop" isn't in the hardcoded section
result, actions = self.processor.process_text("the end full stop")
self.assertIn(".", result)
def test_generic_text_command_non_punctuation(self):
"""Test non-punctuation text commands through generic path."""
# Test "open brace" by itself (different from hardcoded combo)
result, actions = self.processor.process_text("code open brace here")
self.assertIn("{", result)
def test_generic_format_capitalize_next(self):
"""Test capitalize format command through generic path."""
# Use a different pattern than hardcoded cases
result, actions = self.processor.process_text("capitalize john")
# Should capitalize john -> John
self.assertIn("John", result)
def test_generic_format_uppercase_next(self):
"""Test uppercase format command through generic path."""
# "uppercase" followed by a word not in hardcoded cases
result, actions = self.processor.process_text("uppercase hello")
self.assertIn("HELLO", result)
def test_generic_format_lowercase_next(self):
"""Test lowercase format command through generic path."""
# Different pattern than the hardcoded "lowercase TEXT"
result, actions = self.processor.process_text("lowercase WORLD")
self.assertIn("world", result)
def test_generic_format_no_target_word(self):
"""Test format command with no following word through generic path."""
# "uppercase" alone - no word follows
result, actions = self.processor.process_text("uppercase")
# Should return empty when no target word
self.assertEqual(result, "")
def test_generic_multiple_punctuation(self):
"""Test multiple punctuation commands through generic path."""
result, actions = self.processor.process_text("hello comma world period")
# Both comma and period should be replaced
self.assertIn(",", result)
self.assertIn(".", result)
def test_generic_semicolon(self):
"""Test semicolon through generic path."""
result, actions = self.processor.process_text("statement semicolon")
self.assertIn(";", result)
def test_generic_colon(self):
"""Test colon through generic path."""
result, actions = self.processor.process_text("note colon important")
self.assertIn(":", result)
def test_generic_question_mark(self):
"""Test question mark through generic path."""
result, actions = self.processor.process_text("really question mark")
self.assertIn("?", result)
def test_generic_exclamation_point(self):
"""Test exclamation point through generic path."""
result, actions = self.processor.process_text("wow exclamation point")
self.assertIn("!", result)
def test_generic_unknown_format_type(self):
"""Test unknown format type falls back to word as-is."""
# Add an unknown format type for testing
self.processor.format_commands["testformat"] = "unknown_type"
self.processor._compile_patterns()
result, actions = self.processor.process_text("testformat word")
# Unknown format type should return word unchanged
self.assertIn("word", result.lower())
def test_generic_select_line_action(self):
"""Test select line action through generic path."""
result, actions = self.processor.process_text("select line now")
self.assertIn("select_line", actions)
def test_generic_select_word_action(self):
"""Test select word action through generic path."""
result, actions = self.processor.process_text("select word please")
self.assertIn("select_word", actions)
def test_generic_cut_action(self):
"""Test cut action through generic path."""
result, actions = self.processor.process_text("cut selection")
self.assertIn("cut", actions)
def test_generic_paste_action(self):
"""Test paste action through generic path."""
result, actions = self.processor.process_text("paste content")
self.assertIn("paste", actions)