start implementation on new architecture

This commit is contained in:
2024-11-01 09:56:30 +01:00
parent ee089e5be7
commit 2c1e134c6e
43 changed files with 2978 additions and 619 deletions

View File

@@ -0,0 +1,227 @@
import unittest
from datetime import datetime
import os
import time
import signal
import platform
from sia.background_entry import BackgroundEntry
class BackgroundEntryTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with fixed id and timestamp"""
self.test_id = "test-id-1234"
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
# Create temporary files for testing
self.test_filename = "test_output.txt"
self.counter_file = "counter.txt"
self.cleanup_files()
def tearDown(self):
"""Clean up test files and ensure no processes are left running"""
self.cleanup_files()
def cleanup_files(self):
"""Helper to remove test files"""
for filename in [self.test_filename, self.counter_file]:
if os.path.exists(filename):
os.remove(filename)
def wait_for_process_start(self, entry, timeout=1.0):
"""Wait for process to start, with timeout"""
start_time = time.time()
while entry.process is None and time.time() - start_time < timeout:
entry.update()
time.sleep(0.1)
self.assertIsNotNone(entry.process, "Process failed to start")
def wait_for_process_exit(self, entry, timeout=1.0):
"""Wait for process to exit, with timeout"""
start_time = time.time()
while entry.process is not None and time.time() - start_time < timeout:
entry.update()
time.sleep(0.1)
self.assertIsNone(entry.process, "Process failed to exit")
def test_initialization(self):
"""Test entry initialization"""
script = "echo test"
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
self.assertEqual(entry.script, script)
self.assertEqual(entry.stdout, "")
self.assertEqual(entry.stderr, "")
self.assertIsNone(entry.process)
self.assertEqual(entry.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp)
def test_short_running_process(self):
"""Test execution of a quick process"""
script = "echo 'test'"
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Start and wait for completion
entry.update()
self.wait_for_process_start(entry)
self.wait_for_process_exit(entry)
# Verify output
self.assertEqual(entry._accumulated_stdout.strip(), "test")
self.assertEqual(entry._accumulated_stderr, "")
self.assertEqual(entry._exit_code, 0)
def test_continuous_output(self):
"""Test process that generates continuous output"""
# Create a Python script that counts to 3 with minimal delay
script = (
'python3 -c "'
'import sys\n'
'for i in range(3):\n'
' sys.stdout.write(f\\\"count {i+1}\\n\\\")\n'
' sys.stdout.flush()\n'
'"'
)
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Start process and wait for completion
entry.update()
self.wait_for_process_start(entry)
self.wait_for_process_exit(entry, timeout=2.0)
# One more update to ensure we get all output
entry.update()
# Verify all output was captured
output = entry._accumulated_stdout
self.assertIn('count 1', output, "Missing count 1")
self.assertIn('count 2', output, "Missing count 2")
self.assertIn('count 3', output, "Missing count 3")
def test_process_failure(self):
"""Test handling of process failures"""
entry = BackgroundEntry("nonexistentcommand", self.test_id, self.test_timestamp)
# Start and wait for failure
entry.update()
self.wait_for_process_exit(entry)
# Verify error was captured
self.assertTrue(len(entry._accumulated_stderr) > 0)
self.assertEqual(entry._accumulated_stdout, "")
self.assertNotEqual(entry._exit_code, 0)
def test_generate_context_running(self):
"""Test XML context generation for running process"""
script = "sleep 1"
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
entry.update() # Start process
self.wait_for_process_start(entry)
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "background")
self.assertEqual(element.get("id"), self.test_id)
self.assertIsNotNone(element.get("pid"))
self.assertIsNone(element.get("exit_code"))
# Clean up
entry._cleanup_process()
def test_generate_context_completed(self):
"""Test XML context generation for completed process"""
script = "echo 'test'"
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Start and wait for completion
entry.update()
self.wait_for_process_start(entry)
self.wait_for_process_exit(entry)
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "background")
self.assertEqual(element.get("id"), self.test_id)
self.assertIsNone(element.get("pid"))
self.assertEqual(element.get("exit_code"), "0")
stdout_text = element.find("stdout").text
self.assertTrue(stdout_text.startswith("<![CDATA["))
self.assertIn("test", stdout_text)
self.assertTrue(stdout_text.endswith("]]>"))
def test_cleanup_on_deletion(self):
"""Test process cleanup when entry is deleted"""
script = "sleep 10"
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
entry.update()
self.wait_for_process_start(entry)
# Verify process is running
self.assertIsNotNone(entry.process)
pid = entry.process.pid
# Delete the entry
entry._cleanup_process()
del entry
# Give process time to be cleaned up
time.sleep(0.1)
# Verify process was terminated
with self.assertRaises(ProcessLookupError):
os.kill(pid, 0) # Check if process exists
def test_cleanup_running_process(self):
"""Test cleanup of running process"""
script = "sleep 10"
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Start the process
entry.update()
self.assertIsNotNone(entry.process)
process_pid = entry.process.pid
# Cleanup should terminate process
entry.cleanup()
# Verify process was terminated
self.assertIsNone(entry.process)
time.sleep(0.1) # Give process time to terminate
with self.assertRaises(ProcessLookupError):
import os
os.kill(process_pid, 0)
def test_cleanup_completed_process(self):
"""Test cleanup of already completed process"""
script = "echo 'test'"
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Run and wait for completion
entry.update()
self.wait_for_process_exit(entry)
# Cleanup should be safe for completed process
entry.cleanup()
self.assertIsNone(entry.process)
def test_multiple_cleanup_calls(self):
"""Test that multiple cleanup calls are safe"""
script = "sleep 10"
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Start process
entry.update()
self.assertIsNotNone(entry.process)
# Multiple cleanups should be safe
entry.cleanup()
entry.cleanup()
entry.cleanup()
self.assertIsNone(entry.process)

View File

@@ -1,33 +0,0 @@
import unittest
from datetime import datetime
from sia.context_template import generate_context
class TestContextTemplate(unittest.TestCase):
def test_empty_containers(self):
"""Test context generation with no containers."""
context = generate_context([])
self.assertIn("<context>", context)
self.assertIn("<containers/>", context)
self.assertIn("</context>", context)
def test_single_container(self):
"""Test context generation with a single container."""
container_status = {
'name': 'test-container',
'status': 'running',
'started_at': '2024-10-25T10:00:00Z',
'stdout_size': 100,
'stderr_size': 50
}
context = generate_context([container_status])
self.assertIn("<context>", context)
self.assertIn("<containers", context)
self.assertIn('name="test-container"', context)
self.assertIn('status="running"', context)
self.assertIn('started_at="2024-10-25T10:00:00Z"', context)
self.assertIn('stdout="100"', context)
self.assertIn('stderr="50"', context)
self.assertIn("</context>", context)
if __name__ == '__main__':
unittest.main()

View File

@@ -0,0 +1,66 @@
import unittest
from datetime import datetime
import time
from sia.working_memory import WorkingMemory
from sia.delete_command import DeleteCommand
from sia.reasoning_entry import ReasoningEntry
from sia.background_entry import BackgroundEntry
class DeleteCommandTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with a fresh working memory"""
self.memory = WorkingMemory()
self.test_id = "test-id-1234"
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
def test_delete_existing_entry(self):
"""Test deleting an existing entry"""
# Add test entry
entry = ReasoningEntry("test content", self.test_id, self.test_timestamp)
self.memory.add_entry(entry)
# Execute delete command
command = DeleteCommand(self.test_id)
result = command.execute(self.memory)
# Verify result and memory state
self.assertTrue(result.success)
self.assertFalse(result.should_stop)
self.assertEqual(result.message, "")
self.assertEqual(self.memory.get_entries_count(), 0)
def test_delete_running_background_entry(self):
"""Test deleting a background entry that is still running"""
# Create a background entry with a long-running command
entry = BackgroundEntry("sleep 10", self.test_id, self.test_timestamp)
self.memory.add_entry(entry)
# Start the process
entry.update()
self.assertIsNotNone(entry.process)
process_pid = entry.process.pid
# Execute delete command
command = DeleteCommand(self.test_id)
result = command.execute(self.memory)
# Verify process was terminated
time.sleep(0.1) # Give process time to terminate
self.assertTrue(result.success)
self.assertIsNone(entry.process)
# Verify process no longer exists
with self.assertRaises(ProcessLookupError):
import os
os.kill(process_pid, 0)
def test_delete_nonexistent_entry(self):
"""Test attempting to delete a nonexistent entry"""
command = DeleteCommand("nonexistent-id")
result = command.execute(self.memory)
# Verify error result
self.assertFalse(result.success)
self.assertFalse(result.should_stop)
self.assertIn("not found", result.message)

View File

@@ -1,121 +0,0 @@
import unittest
import time
from sia.docker_module import DockerModule
class DockerModuleTest(unittest.TestCase):
def test_initialization(self):
"""Test DockerModule initialization."""
with DockerModule() as docker_module:
self.assertIsInstance(docker_module, DockerModule)
def test_start_container_short_lived(self):
"""Test starting a short-lived container."""
with DockerModule() as docker_module:
image = "busybox:latest"
timeout = 1000
command = "echo"
arguments = ["Hello World"]
output = docker_module.start_container(
image=image,
timeout=timeout,
command=command,
arguments=arguments
)
self.assertEqual(output, "Hello World\n")
def test_start_container_long_running(self):
"""Test starting a long-running container."""
with DockerModule() as docker_module:
name = "test_start_container_long_running"
image = "busybox:latest"
container_name = docker_module.start_container(
image=image,
name=name
)
self.assertEqual(container_name, name)
self.assertIn(name, docker_module.containers)
docker_module.wait_container(container_name, 0)
self.assertNotIn(name, docker_module.containers)
def test_start_container_with_volumes(self):
"""Test starting a container with volume mappings."""
with DockerModule() as docker_module:
arguments = ["sh", "-c", "echo 'Hello World' > /write_here/test_start_container_with_volumes"]
volumes = {"/tmp": "/write_here"}
docker_module.start_container(
image="busybox:latest",
timeout=1000,
arguments=arguments,
volumes=volumes
)
with open("/tmp/test_start_container_with_volumes", "r") as f:
self.assertEqual(f.read(), "Hello World\n")
def test_invalid_container_start(self):
"""Test error handling for invalid container start."""
with DockerModule() as docker_module:
with self.assertRaises(ValueError):
docker_module.start_container(image="busybox:latest")
def test_container_io_operations(self):
"""Test container I/O operations."""
with DockerModule() as docker_module:
arguments = [
"sh",
"-c",
"read input; echo \"$input\" | tr '[:lower:]' '[:upper:]'; echo \"$input\" | tr '[:upper:]' '[:lower:]' >&2"
]
name = "test_container_io_operations"
container_name = docker_module.start_container(
image="busybox:latest",
name=name,
arguments=arguments
)
test_input = "Test Input\n"
docker_module.write_container_stdin(name, test_input)
time.sleep(1) # for processing
stdout = docker_module.read_container_stdout(name)
self.assertEqual(stdout, "TEST INPUT\n")
stderr = docker_module.read_container_stderr(name)
self.assertEqual(stderr, "test input\n")
docker_module.wait_container(container_name, 0)
def test_wait_container(self):
"""Test waiting for container completion."""
with DockerModule() as docker_module:
name = "test_wait_container"
docker_module.start_container(
image="busybox:latest",
name=name
)
exit_code, output = docker_module.wait_container(name, timeout=1000)
self.assertEqual(exit_code, -1)
self.assertEqual(output, "")
def test_get_container_status(self):
"""Test getting container status information."""
with DockerModule() as docker_module:
arguments = [
"sh",
"-c",
"echo 'standard output'; echo 'standard error' >&2; sleep 3"
]
name = "test_get_container_status"
docker_module.start_container(
image="busybox:latest",
name=name,
arguments=arguments
)
time.sleep(1)
status = docker_module.get_container_status(name)
self.assertEqual(status['name'], name)
self.assertEqual(status['status'], "running")
self.assertGreater(len(status['started_at']), 0)
self.assertEqual(status['exit_code'], 0)
self.assertEqual(status['error'], "")
self.assertEqual(status['stdout_size'], 16)
self.assertEqual(status['stderr_size'], 15)
docker_module.wait_container(name, 0)
if __name__ == '__main__':
unittest.main()

View File

@@ -3,7 +3,6 @@ import unittest
from itertools import tee
from . import test_data
from . import test_util
from sia.llm_engine import LlmEngine

View File

@@ -0,0 +1,88 @@
import unittest
from datetime import datetime
from sia.parse_error_entry import ParseErrorEntry
class ParseErrorEntryTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with fixed id and timestamp"""
self.test_id = "test-id-1234"
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
def test_initialization(self):
"""Test entry initialization"""
content = "invalid content"
error = "parsing failed"
entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp)
self.assertEqual(entry.content, content)
self.assertEqual(entry.error, error)
self.assertEqual(entry.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp)
def test_update_does_nothing(self):
"""Test that update operation has no effect"""
content = "invalid content"
error = "parsing failed"
entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp)
# Store initial state
initial_content = entry.content
initial_error = entry.error
# Perform update
entry.update()
# Verify state hasn't changed
self.assertEqual(entry.content, initial_content)
self.assertEqual(entry.error, initial_error)
def test_generate_context(self):
"""Test XML context generation"""
content = "invalid content"
error = "parsing failed"
entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp)
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "parse_error")
self.assertEqual(element.get("id"), self.test_id)
# Verify error element
error_elem = element.find("error")
self.assertIsNotNone(error_elem)
self.assertEqual(error_elem.text, f"<![CDATA[{error}]]>")
# Verify content element
content_elem = element.find("content")
self.assertIsNotNone(content_elem)
self.assertEqual(content_elem.text, f"<![CDATA[{content}]]>")
def test_special_characters(self):
"""Test handling content and errors with special characters"""
content = "Special content: \n\t\r\'\"\\"
error = "Special error: \n\t\r\'\"\\"
entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp)
element = entry.generate_context()
self.assertEqual(element.find("content").text, f"<![CDATA[{content}]]>")
self.assertEqual(element.find("error").text, f"<![CDATA[{error}]]>")
def test_empty_content_and_error(self):
"""Test handling empty content and error messages"""
entry = ParseErrorEntry("", "", self.test_id, self.test_timestamp)
element = entry.generate_context()
self.assertEqual(element.find("content").text, "<![CDATA[]]>")
self.assertEqual(element.find("error").text, "<![CDATA[]]>")
def test_large_content(self):
"""Test handling large content and error messages"""
content = "x" * 10000
error = "y" * 10000
entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp)
element = entry.generate_context()
self.assertEqual(element.find("content").text, f"<![CDATA[{content}]]>")
self.assertEqual(element.find("error").text, f"<![CDATA[{error}]]>")

130
test/read_entry_test.py Normal file
View File

@@ -0,0 +1,130 @@
import unittest
from datetime import datetime
from sia.web_io_buffer import WebIOBuffer
from sia.read_entry import ReadEntry
class ReadEntryTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with fixed id, timestamp, and IO buffer"""
self.test_id = "test-id-1234"
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
self.io_buffer = WebIOBuffer()
def test_initialization(self):
"""Test entry initialization"""
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
self.assertEqual(entry.content, "")
self.assertEqual(entry.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp)
self.assertFalse(entry._read)
def test_single_read(self):
"""Test reading content once"""
test_input = "test message"
self.io_buffer.append_stdin(test_input)
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
# Verify content was read
self.assertEqual(entry.content, test_input)
self.assertTrue(entry._read)
self.assertEqual(self.io_buffer.buffer_length(), 0) # Buffer should be cleared
def test_multiple_updates(self):
"""Test that content is only read once even with multiple updates"""
test_input = "initial input"
self.io_buffer.append_stdin(test_input)
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
initial_content = entry.content
# Add more input and update again
self.io_buffer.append_stdin("additional input")
entry.update()
entry.update()
# Verify content hasn't changed after first read
self.assertEqual(entry.content, initial_content)
self.assertTrue(entry._read)
def test_empty_input(self):
"""Test reading when no input is available"""
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(entry.content, "")
self.assertTrue(entry._read)
def test_special_characters(self):
"""Test reading content with special characters"""
test_input = "Special chars: \n\t\r\'\"\\"
self.io_buffer.append_stdin(test_input)
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(entry.content, test_input)
def test_large_content(self):
"""Test reading large content"""
test_input = "x" * 10000
self.io_buffer.append_stdin(test_input)
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(entry.content, test_input)
def test_generate_context_before_read(self):
"""Test XML context generation before reading"""
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "read_stdin")
self.assertEqual(element.get("id"), self.test_id)
self.assertIsNone(element.text) # No content before read
def test_generate_context_after_read(self):
"""Test XML context generation after reading"""
test_input = "test message"
self.io_buffer.append_stdin(test_input)
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "read_stdin")
self.assertEqual(element.get("id"), self.test_id)
self.assertEqual(element.text, f"<![CDATA[{test_input}]]>")
def test_unicode_content(self):
"""Test reading Unicode content"""
test_input = "Hello 世界 😊"
self.io_buffer.append_stdin(test_input)
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(entry.content, test_input)
def test_multiline_content(self):
"""Test reading multiline content"""
test_input = """Line 1
Line 2
Line 3"""
self.io_buffer.append_stdin(test_input)
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(entry.content, test_input)
# Verify XML escaping for multiline content
element = entry.generate_context()
self.assertEqual(element.text, f"<![CDATA[{test_input}]]>")

View File

@@ -0,0 +1,78 @@
import unittest
from datetime import datetime
from sia.reasoning_entry import ReasoningEntry
class ReasoningEntryTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with fixed id and timestamp"""
self.test_id = "test-id-1234"
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
def test_initialization(self):
"""Test entry initialization"""
content = "test reasoning"
entry = ReasoningEntry(content, self.test_id, self.test_timestamp)
self.assertEqual(entry.content, content)
self.assertEqual(entry.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp)
def test_update_does_nothing(self):
"""Test that update operation has no effect"""
content = "test reasoning"
entry = ReasoningEntry(content, self.test_id, self.test_timestamp)
# Store initial state
initial_content = entry.content
# Perform update
entry.update()
# Verify state hasn't changed
self.assertEqual(entry.content, initial_content)
def test_generate_context(self):
"""Test XML context generation"""
content = "test reasoning"
entry = ReasoningEntry(content, self.test_id, self.test_timestamp)
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "reasoning")
self.assertEqual(element.get("id"), self.test_id)
self.assertEqual(element.text, f"<![CDATA[{content}]]>")
def test_special_characters(self):
"""Test handling reasoning text with special characters"""
content = "Special reasoning: \n\t\r\'\"\\"
entry = ReasoningEntry(content, self.test_id, self.test_timestamp)
element = entry.generate_context()
self.assertEqual(element.text, f"<![CDATA[{content}]]>")
def test_empty_content(self):
"""Test handling empty reasoning text"""
entry = ReasoningEntry("", self.test_id, self.test_timestamp)
element = entry.generate_context()
self.assertEqual(element.text, "<![CDATA[]]>")
def test_large_content(self):
"""Test handling large reasoning text"""
content = "x" * 10000
entry = ReasoningEntry(content, self.test_id, self.test_timestamp)
element = entry.generate_context()
self.assertEqual(element.text, f"<![CDATA[{content}]]>")
def test_multiline_content(self):
"""Test handling multiline reasoning text"""
content = """Line 1
Line 2
Line 3"""
entry = ReasoningEntry(content, self.test_id, self.test_timestamp)
element = entry.generate_context()
self.assertEqual(element.text, f"<![CDATA[{content}]]>")

164
test/repeat_entry_test.py Normal file
View File

@@ -0,0 +1,164 @@
import unittest
from datetime import datetime
import os
from sia.repeat_entry import RepeatEntry
class RepeatEntryTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with fixed id and timestamp"""
self.test_id = "test-id-1234"
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
# Create a temporary file for testing
self.test_filename = "test_output.txt"
self.counter_file = "counter.txt"
self.cleanup_files()
def tearDown(self):
"""Clean up any test files"""
self.cleanup_files()
def cleanup_files(self):
"""Helper to remove test files"""
for filename in [self.test_filename, self.counter_file]:
if os.path.exists(filename):
os.remove(filename)
def test_initialization(self):
"""Test entry initialization"""
script = "echo test"
entry = RepeatEntry(script, self.test_id, self.test_timestamp)
self.assertEqual(entry.script, script)
self.assertEqual(entry.stdout, "")
self.assertEqual(entry.stderr, "")
self.assertIsNone(entry.exit_code)
self.assertEqual(entry.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp)
def test_successful_execution(self):
"""Test successful script execution"""
script = "echo 'test'"
entry = RepeatEntry(script, self.test_id, self.test_timestamp)
entry.update()
# Verify entry state
self.assertEqual(entry.stdout.strip(), "test")
self.assertEqual(entry.stderr, "")
self.assertEqual(entry.exit_code, 0)
def test_failed_execution(self):
"""Test failed script execution with invalid command"""
entry = RepeatEntry("nonexistentcommand", self.test_id, self.test_timestamp)
entry.update()
# Verify entry state
self.assertEqual(entry.stdout, "")
self.assertTrue(len(entry.stderr) > 0) # Error message will vary by OS
self.assertNotEqual(entry.exit_code, 0) # Exit code will be non-zero
def test_repeated_execution(self):
"""Test that script executes on every update"""
# Create a counter file
with open(self.counter_file, 'w') as f:
f.write('0')
# Script that increments and reads the counter
script = (
f'python3 -c "'
f'import os; '
f'f=open(\'{self.counter_file}\', \'r+\'); '
f'n=int(f.read()); '
f'f.seek(0); '
f'f.write(str(n+1)); '
f'f.truncate(); '
f'f.close(); '
f'print(n+1)'
f'"'
)
entry = RepeatEntry(script, self.test_id, self.test_timestamp)
# Run update multiple times
outputs = []
for _ in range(3):
entry.update()
outputs.append(int(entry.stdout.strip()))
# Verify outputs are sequential numbers
self.assertEqual(outputs, [1, 2, 3])
def test_generate_context(self):
"""Test XML context generation"""
script = "echo 'test'"
entry = RepeatEntry(script, self.test_id, self.test_timestamp)
entry.update()
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "repeat")
self.assertEqual(element.get("id"), self.test_id)
self.assertEqual(element.text, f"<![CDATA[{script}]]>")
self.assertEqual(element.get("exit_code"), "0")
# Get stdout and remove trailing newline for comparison
stdout_text = element.find("stdout").text
self.assertTrue(stdout_text.startswith("<![CDATA[test"))
self.assertTrue(stdout_text.endswith("]]>"))
self.assertEqual(element.find("stderr").text, "<![CDATA[]]>")
def test_changing_output(self):
"""Test handling of changing command output"""
# Create a counter file
with open(self.counter_file, 'w') as f:
f.write('0')
# Script that outputs an incrementing number
script = (
f'python3 -c "'
f'import os; '
f'f=open(\'{self.counter_file}\', \'r+\'); '
f'n=int(f.read()); '
f'f.seek(0); '
f'f.write(str(n+1)); '
f'f.truncate(); '
f'f.close(); '
f'print(\'Output\', n+1)'
f'"'
)
entry = RepeatEntry(script, self.test_id, self.test_timestamp)
# Execute multiple times and verify output changes
entry.update()
first_output = entry.stdout.strip()
entry.update()
second_output = entry.stdout.strip()
entry.update()
third_output = entry.stdout.strip()
# Outputs should be different for each run
outputs = [first_output, second_output, third_output]
unique_outputs = set(outputs)
self.assertEqual(len(unique_outputs), 3)
self.assertEqual(outputs, ['Output 1', 'Output 2', 'Output 3'])
def test_error_recovery(self):
"""Test that entry recovers from errors in subsequent updates"""
# Start with a failing command
entry = RepeatEntry("nonexistentcommand", self.test_id, self.test_timestamp)
entry.update()
self.assertNotEqual(entry.exit_code, 0)
# Change to a working command
entry.script = "echo 'test'"
entry.update()
# Verify recovery
self.assertEqual(entry.exit_code, 0)
self.assertEqual(entry.stdout.strip(), "test")
self.assertEqual(entry.stderr, "")

View File

@@ -0,0 +1,141 @@
import unittest
from datetime import datetime
import os
from sia.single_shot_entry import SingleShotEntry
class SingleShotEntryTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with fixed id and timestamp"""
self.test_id = "test-id-1234"
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
# Create a temporary file for testing
self.test_filename = "test_output.txt"
self.cleanup_files()
def tearDown(self):
"""Clean up any test files"""
self.cleanup_files()
def cleanup_files(self):
"""Helper to remove test files"""
if os.path.exists(self.test_filename):
os.remove(self.test_filename)
def test_initialization(self):
"""Test entry initialization"""
script = "echo test"
entry = SingleShotEntry(script, self.test_id, self.test_timestamp)
self.assertEqual(entry.script, script)
self.assertEqual(entry.stdout, "")
self.assertEqual(entry.stderr, "")
self.assertIsNone(entry.exit_code)
self.assertEqual(entry.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp)
self.assertFalse(entry._executed)
def test_successful_execution(self):
"""Test successful script execution"""
script = "echo 'test'"
entry = SingleShotEntry(script, self.test_id, self.test_timestamp)
entry.update()
# Verify entry state
self.assertEqual(entry.stdout.strip(), "test")
self.assertEqual(entry.stderr, "")
self.assertEqual(entry.exit_code, 0)
self.assertTrue(entry._executed)
def test_failed_execution(self):
"""Test failed script execution with invalid command"""
entry = SingleShotEntry("nonexistentcommand", self.test_id, self.test_timestamp)
entry.update()
# Verify entry state
self.assertEqual(entry.stdout, "")
self.assertTrue(len(entry.stderr) > 0) # Error message will vary by OS
self.assertNotEqual(entry.exit_code, 0) # Exit code will be non-zero
self.assertTrue(entry._executed)
def test_file_creation(self):
"""Test script that creates a file"""
script = f"echo 'test' > {self.test_filename}"
entry = SingleShotEntry(script, self.test_id, self.test_timestamp)
entry.update()
# Verify file was created and contains expected content
self.assertTrue(os.path.exists(self.test_filename))
with open(self.test_filename, 'r') as f:
content = f.read().strip()
self.assertEqual(content, "test")
def test_single_execution(self):
"""Test that script only executes once"""
script = f"echo 'test' >> {self.test_filename}"
entry = SingleShotEntry(script, self.test_id, self.test_timestamp)
# Run update multiple times
entry.update()
entry.update()
entry.update()
# Verify file only contains one line
with open(self.test_filename, 'r') as f:
lines = f.readlines()
self.assertEqual(len(lines), 1)
self.assertEqual(lines[0].strip(), "test")
def test_generate_context_before_execution(self):
"""Test XML context generation before execution"""
entry = SingleShotEntry("echo test", self.test_id, self.test_timestamp)
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "single_shot")
self.assertEqual(element.get("id"), self.test_id)
self.assertEqual(element.text, "<![CDATA[echo test]]>")
# Verify no output elements exist
self.assertIsNone(element.find("stdout"))
self.assertIsNone(element.find("stderr"))
self.assertIsNone(element.get("exit_code"))
def test_generate_context_after_execution(self):
"""Test XML context generation after execution"""
script = "echo 'test'"
entry = SingleShotEntry(script, self.test_id, self.test_timestamp)
entry.update()
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "single_shot")
self.assertEqual(element.get("id"), self.test_id)
self.assertEqual(element.text, f"<![CDATA[{script}]]>")
# Get stdout and remove trailing newline for comparison
stdout_text = element.find("stdout").text
self.assertTrue(stdout_text.startswith("<![CDATA[test"))
self.assertTrue(stdout_text.endswith("]]>"))
self.assertEqual(element.find("stderr").text, "<![CDATA[]]>")
self.assertEqual(element.get("exit_code"), "0")
def test_multiple_commands(self):
"""Test executing multiple commands in one script"""
script = f"echo 'first' > {self.test_filename} && echo 'second' >> {self.test_filename}"
entry = SingleShotEntry(script, self.test_id, self.test_timestamp)
entry.update()
# Verify file contains both lines
with open(self.test_filename, 'r') as f:
lines = f.readlines()
self.assertEqual(len(lines), 2)
self.assertEqual(lines[0].strip(), "first")
self.assertEqual(lines[1].strip(), "second")

View File

@@ -0,0 +1,180 @@
import unittest
import os
import subprocess
import sys
import tempfile
from pathlib import Path
class StandardIOBufferTest(unittest.TestCase):
def setUp(self):
"""Create temporary directory for test files"""
self.test_dir = tempfile.mkdtemp()
self.sia_path = os.path.abspath(os.path.join(os.path.dirname(__file__), '..'))
self.write_test_script()
def tearDown(self):
"""Clean up temporary test files"""
for file in Path(self.test_dir).glob('*'):
file.unlink()
os.rmdir(self.test_dir)
def write_test_script(self):
"""Create a Python script that uses StandardIOBuffer"""
script_path = os.path.join(self.test_dir, 'test_script.py')
with open(script_path, 'w', newline='') as f:
f.write(f'''
import sys
import os
import time
# Add the sia package directory to Python path
sys.path.insert(0, {repr(self.sia_path)})
from sia.standard_io_buffer import StandardIOBuffer
def run_buffer_test(test_type):
buffer = StandardIOBuffer()
if test_type == 'read':
content = buffer.read()
sys.stdout.buffer.write(f"Read: {{content}}".encode('utf-8'))
sys.stdout.buffer.flush()
elif test_type == 'write':
buffer.write("Test output")
sys.stdout.buffer.flush()
elif test_type == 'multiple_write':
buffer.write("Part 1")
buffer.write("Part 2")
sys.stdout.buffer.flush()
elif test_type == 'buffer_length':
buffer._input_buffer = "test"
sys.stdout.buffer.write(str(buffer.buffer_length()).encode('utf-8'))
sys.stdout.buffer.flush()
elif test_type == 'read_lines':
content1 = buffer.read()
sys.stdout.buffer.write(f"First read: {{content1}}".encode('utf-8'))
sys.stdout.buffer.flush()
time.sleep(0.1)
content2 = buffer.read()
sys.stdout.buffer.write(f"Second read: {{content2}}".encode('utf-8'))
sys.stdout.buffer.flush()
elif test_type == 'debug':
print("Python path:", sys.path)
print("Current directory:", os.getcwd())
print("Buffer import successful")
print("Python version:", sys.version)
print("Platform:", sys.platform)
if __name__ == '__main__':
if len(sys.argv) < 2:
print("Test type required")
sys.exit(1)
test_type = sys.argv[1]
run_buffer_test(test_type)
''')
self.script_path = script_path
def run_script(self, test_type, input_data=None):
"""Run test script with given input and return output"""
process = subprocess.Popen(
[sys.executable, self.script_path, test_type],
stdin=subprocess.PIPE,
stdout=subprocess.PIPE,
stderr=subprocess.PIPE,
env={**os.environ, 'PYTHONPATH': self.sia_path},
# Use binary mode for consistent line endings
universal_newlines=False
)
try:
# Convert input data to bytes if provided
input_bytes = input_data.encode('utf-8') if input_data is not None else None
stdout_bytes, stderr_bytes = process.communicate(input=input_bytes, timeout=5)
# Decode output using utf-8, preserving special characters
stdout = stdout_bytes.decode('utf-8')
stderr = stderr_bytes.decode('utf-8')
return stdout, stderr, process.returncode
except subprocess.TimeoutExpired:
process.kill()
stdout_bytes, stderr_bytes = process.communicate()
stdout = stdout_bytes.decode('utf-8')
stderr = stderr_bytes.decode('utf-8')
return stdout, stderr, -1
def test_debug_environment(self):
"""Test that the environment is properly set up"""
stdout, stderr, code = self.run_script('debug')
self.assertEqual(code, 0, f"Debug script failed with stderr: {stderr}")
self.assertIn("Buffer import successful", stdout)
def test_read_available_input(self):
"""Test reading available input without mocks"""
stdout, stderr, code = self.run_script('read', 'test input\n')
self.assertEqual(code, 0, f"Script failed with stderr: {stderr}")
self.assertEqual(stdout, 'Read: test input\n')
def test_read_no_input(self):
"""Test reading when no input is provided"""
stdout, stderr, code = self.run_script('read')
self.assertEqual(code, 0, f"Script failed with stderr: {stderr}")
self.assertEqual(stdout, 'Read: ')
def test_write_output(self):
"""Test writing output without mocks"""
stdout, stderr, code = self.run_script('write')
self.assertEqual(code, 0, f"Script failed with stderr: {stderr}")
self.assertEqual(stdout, 'Test output')
def test_multiple_writes(self):
"""Test multiple write operations"""
stdout, stderr, code = self.run_script('multiple_write')
self.assertEqual(code, 0, f"Script failed with stderr: {stderr}")
self.assertEqual(stdout, 'Part 1Part 2')
def test_buffer_length(self):
"""Test buffer length reporting"""
stdout, stderr, code = self.run_script('buffer_length')
self.assertEqual(code, 0, f"Script failed with stderr: {stderr}")
self.assertEqual(stdout.strip(), '4')
def test_multiple_reads(self):
"""Test multiple read operations with actual input"""
input_data = 'line1\nline2\n'
stdout, stderr, code = self.run_script('read_lines', input_data)
self.assertEqual(code, 0, f"Script failed with stderr: {stderr}")
self.assertIn('First read: line1\nline2\n', stdout)
self.assertIn('Second read: ', stdout)
def test_special_characters(self):
"""Test handling special characters in input/output"""
# Use raw string to ensure exact character representation
special_chars = 'Special chars: \n\t\r\'"'
stdout, stderr, code = self.run_script('read', special_chars)
self.assertEqual(code, 0, f"Script failed with stderr: {stderr}")
expected = f'Read: {special_chars}'
# Print actual bytes for debugging
if stdout != expected:
print("\nDebug output:")
print("Expected bytes:", expected.encode('utf-8'))
print("Actual bytes:", stdout.encode('utf-8'))
self.assertEqual(stdout, expected)
def test_empty_input(self):
"""Test handling empty input"""
stdout, stderr, code = self.run_script('read', '')
self.assertEqual(code, 0, f"Script failed with stderr: {stderr}")
self.assertEqual(stdout, 'Read: ')
def test_large_input(self):
"""Test handling large input"""
large_input = 'x' * 10000
stdout, stderr, code = self.run_script('read', large_input)
self.assertEqual(code, 0, f"Script failed with stderr: {stderr}")
self.assertEqual(stdout, f'Read: {large_input}')

45
test/stop_command_test.py Normal file
View File

@@ -0,0 +1,45 @@
import unittest
from datetime import datetime
import time
from sia.working_memory import WorkingMemory
from sia.stop_command import StopCommand
from sia.reasoning_entry import ReasoningEntry
from sia.background_entry import BackgroundEntry
class StopCommandTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with a fresh working memory"""
self.memory = WorkingMemory()
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
def test_stop_command_cleanup(self):
"""Test stop command cleans up and removes all entries"""
# Add a mix of entry types including a background process
entries = [
ReasoningEntry("test content", "id-1", self.test_timestamp),
BackgroundEntry("sleep 10", "id-2", self.test_timestamp)
]
for entry in entries:
self.memory.add_entry(entry)
# Start the background process
background_entry = self.memory.get_entry("id-2")
background_entry.update()
self.assertIsNotNone(background_entry.process)
process_pid = background_entry.process.pid
# Execute stop command
command = StopCommand()
result = command.execute(self.memory)
# Verify all entries removed
self.assertTrue(result.should_stop)
self.assertEqual(self.memory.get_entries_count(), 0)
# Verify background process was terminated
time.sleep(0.1) # Give process time to terminate
with self.assertRaises(ProcessLookupError):
import os
os.kill(process_pid, 0)

View File

@@ -1,8 +0,0 @@
import unittest
class SequentialTestLoader(unittest.TestLoader):
def getTestCaseNames(self, testCaseClass):
test_names = super().getTestCaseNames(testCaseClass)
testcase_methods = list(testCaseClass.__dict__.keys())
test_names.sort(key=testcase_methods.index)
return test_names

View File

@@ -1,17 +1,118 @@
import unittest
from typing import Iterator
from . import test_data
from sia import util
class UtilTest(unittest.TestCase):
def test_get_valid_root_elements_single(self):
valid_elements = util.get_valid_root_elements(test_data.echo_action_schema)
self.assertEqual(valid_elements, {'test_tag'})
def test_stop_before_value(self):
# Helper function to create iterator from list
def create_iterator(items: list) -> Iterator[str]:
for item in items:
yield item
def test_split_response_single_element(self):
response = "Some reasoning here\n<test_tag>content</test_tag>"
valid_elements = {'test_tag'}
result = util.split_response(response, valid_elements)
self.assertEqual(result.reasoning, "Some reasoning here")
self.assertEqual(result.actions, "<test_tag>content</test_tag>")
# Test case 1: Stop value in middle of sequence
input_sequence = ['hello', 'world', 'STOP', 'ignored']
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
self.assertEqual(result, ['hello', 'world'])
# Test case 2: Stop value not in sequence
input_sequence = ['hello', 'world']
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
self.assertEqual(result, ['hello', 'world'])
# Test case 3: Stop value at start of sequence
input_sequence = ['STOP', 'ignored']
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
self.assertEqual(result, [])
# Test case 4: Stop value as part of an item
input_sequence = ['hello', 'woSTOPrld', 'ignored']
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
self.assertEqual(result, ['hello', 'wo'])
# Test case 5: Empty sequence
input_sequence = []
result = list(util.stop_before_value(create_iterator(input_sequence), 'STOP'))
self.assertEqual(result, [])
def test_none_value(self):
"""Test that None values are converted to empty strings"""
self.assertEqual(util.escape_text_for_xml(None), '')
def test_number_values(self):
"""Test that numbers are properly converted to strings and wrapped in CDATA"""
self.assertEqual(util.escape_text_for_xml(42), '<![CDATA[42]]>')
self.assertEqual(util.escape_text_for_xml(3.14), '<![CDATA[3.14]]>')
self.assertEqual(util.escape_text_for_xml(-100), '<![CDATA[-100]]>')
self.assertEqual(util.escape_text_for_xml(0), '<![CDATA[0]]>')
def test_simple_strings(self):
"""Test that simple strings are wrapped in CDATA"""
self.assertEqual(util.escape_text_for_xml('hello'), '<![CDATA[hello]]>')
self.assertEqual(util.escape_text_for_xml(''), '<![CDATA[]]>')
self.assertEqual(util.escape_text_for_xml(' '), '<![CDATA[ ]]>')
def test_strings_with_special_characters(self):
"""Test strings containing XML special characters but no CDATA end sequence"""
self.assertEqual(util.escape_text_for_xml('<hello>'), '<![CDATA[<hello>]]>')
self.assertEqual(util.escape_text_for_xml('a & b'), '<![CDATA[a & b]]>')
self.assertEqual(util.escape_text_for_xml('"quoted"'), '<![CDATA["quoted"]]>')
self.assertEqual(util.escape_text_for_xml("'single'"), "<![CDATA['single']]>")
self.assertEqual(util.escape_text_for_xml('<![CDATA['), '<![CDATA[<![CDATA[]]>')
def test_strings_with_cdata_end_sequence(self):
"""Test strings containing CDATA end sequence are properly escaped"""
# Simple case with just the CDATA end sequence
self.assertEqual(util.escape_text_for_xml(']]>'), ']]&gt;')
# CDATA end sequence with other special characters
self.assertEqual(
util.escape_text_for_xml('Text with ]]> and <tags> & ampersands'),
'Text with ]]&gt; and &lt;tags&gt; &amp; ampersands'
)
# Multiple CDATA end sequences
self.assertEqual(
util.escape_text_for_xml('Multiple ]]> end ]]> sequences'),
'Multiple ]]&gt; end ]]&gt; sequences'
)
def test_multiline_strings(self):
"""Test multiline strings are properly handled"""
multiline = """Line 1
Line 2
Line 3"""
self.assertEqual(util.escape_text_for_xml(multiline), '<![CDATA[Line 1\n Line 2\n Line 3]]>')
multiline_with_cdata = """Line 1
Line ]]> 2
Line 3"""
self.assertEqual(
util.escape_text_for_xml(multiline_with_cdata),
'Line 1\n Line ]]&gt; 2\n Line 3'
)
def test_edge_cases(self):
"""Test edge cases and unusual inputs"""
# Empty string
self.assertEqual(util.escape_text_for_xml(''), '<![CDATA[]]>')
# String with only whitespace
self.assertEqual(util.escape_text_for_xml('\n\t '), '<![CDATA[\n\t ]]>')
# String with Unicode characters
self.assertEqual(util.escape_text_for_xml('Hello 世界'), '<![CDATA[Hello 世界]]>')
# String with control characters
self.assertEqual(util.escape_text_for_xml('Hello\0World'), '<![CDATA[Hello\0World]]>')
# Very long string
long_string = 'x' * 1000
self.assertEqual(util.escape_text_for_xml(long_string), f'<![CDATA[{long_string}]]>')
def test_boolean_values(self):
"""Test boolean values are properly converted"""
self.assertEqual(util.escape_text_for_xml(True), '<![CDATA[True]]>')
self.assertEqual(util.escape_text_for_xml(False), '<![CDATA[False]]>')

106
test/web_io_buffer_test.py Normal file
View File

@@ -0,0 +1,106 @@
import unittest
from sia.web_io_buffer import WebIOBuffer
class WebIOBufferTest(unittest.TestCase):
def setUp(self):
"""Set up a fresh WebIOBuffer for each test."""
self.buffer = WebIOBuffer()
def test_initialization(self):
"""Test initial state of buffer"""
self.assertEqual(self.buffer.buffer_length(), 0)
self.assertEqual(self.buffer.get_stdout(), "")
self.assertEqual(self.buffer.read(), "")
def test_append_stdin(self):
"""Test appending to stdin buffer"""
test_input = "test input"
self.buffer.append_stdin(test_input)
self.assertEqual(self.buffer.buffer_length(), len(test_input))
def test_read_clears_buffer(self):
"""Test that reading clears the stdin buffer"""
test_input = "test input"
self.buffer.append_stdin(test_input)
content = self.buffer.read()
self.assertEqual(content, test_input)
self.assertEqual(self.buffer.buffer_length(), 0)
self.assertEqual(self.buffer.read(), "")
def test_write_output(self):
"""Test writing to stdout buffer"""
test_output = "test output"
self.buffer.write(test_output)
self.assertEqual(self.buffer.get_stdout(), test_output)
def test_multiple_writes(self):
"""Test multiple write operations"""
self.buffer.write("Part 1")
self.buffer.write("Part 2")
self.assertEqual(self.buffer.get_stdout(), "Part 1Part 2")
def test_clear_stdout(self):
"""Test clearing stdout buffer"""
self.buffer.write("test output")
self.buffer.clear_stdout()
self.assertEqual(self.buffer.get_stdout(), "")
def test_multiple_stdin_appends(self):
"""Test multiple stdin append operations"""
self.buffer.append_stdin("Line 1\n")
self.buffer.append_stdin("Line 2\n")
content = self.buffer.read()
self.assertEqual(content, "Line 1\nLine 2\n")
def test_empty_write(self):
"""Test writing empty content"""
self.buffer.write("")
self.assertEqual(self.buffer.get_stdout(), "")
def test_empty_append_stdin(self):
"""Test appending empty content to stdin"""
self.buffer.append_stdin("")
self.assertEqual(self.buffer.buffer_length(), 0)
def test_special_characters(self):
"""Test handling special characters"""
special_chars = "Special chars: \n\t\r\'\"\\"
self.buffer.append_stdin(special_chars)
content = self.buffer.read()
self.assertEqual(content, special_chars)
self.buffer.write(special_chars)
self.assertEqual(self.buffer.get_stdout(), special_chars)
def test_large_content(self):
"""Test handling large content"""
large_input = "x" * 10000
self.buffer.append_stdin(large_input)
content = self.buffer.read()
self.assertEqual(content, large_input)
self.buffer.write(large_input)
self.assertEqual(self.buffer.get_stdout(), large_input)
def test_multiple_operations(self):
"""Test mixed read/write operations"""
# Write some output
self.buffer.write("Output 1\n")
self.buffer.write("Output 2\n")
# Add some input
self.buffer.append_stdin("Input 1\n")
self.buffer.append_stdin("Input 2\n")
# Verify stdout accumulated correctly
self.assertEqual(self.buffer.get_stdout(), "Output 1\nOutput 2\n")
# Read input and verify it's cleared
content = self.buffer.read()
self.assertEqual(content, "Input 1\nInput 2\n")
self.assertEqual(self.buffer.buffer_length(), 0)
# Clear stdout and verify
self.buffer.clear_stdout()
self.assertEqual(self.buffer.get_stdout(), "")

246
test/working_memory_test.py Normal file
View File

@@ -0,0 +1,246 @@
from datetime import datetime
import time
import unittest
import xml.etree.ElementTree as ET
from sia.background_entry import BackgroundEntry
from sia.entry import Entry
from sia.parse_error_entry import ParseErrorEntry
from sia.read_entry import ReadEntry
from sia.reasoning_entry import ReasoningEntry
from sia.web_io_buffer import WebIOBuffer
from sia.working_memory import WorkingMemory
from sia.write_entry import WriteEntry
class MockEntry(Entry):
"""Mock entry class for testing"""
def __init__(self, id: str, timestamp: datetime):
super().__init__(id, timestamp)
self.updated = False
def update(self) -> None:
self.updated = True
def generate_context(self) -> ET.Element:
elem = ET.Element("mock", {"id": self.id})
elem.text = "<![CDATA[mock content]]>"
return elem
class WorkingMemoryTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with a fresh working memory"""
self.memory = WorkingMemory()
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
def test_initialization(self):
"""Test initial state of working memory"""
self.assertEqual(self.memory.get_entries_count(), 0)
self.assertEqual(self.memory.generate_context(), [])
def test_add_entry(self):
"""Test adding entries"""
entry = MockEntry("test-id-1", self.test_timestamp)
self.memory.add_entry(entry)
self.assertEqual(self.memory.get_entries_count(), 1)
self.assertEqual(self.memory.get_entry("test-id-1"), entry)
def test_add_invalid_entry(self):
"""Test adding invalid entry type"""
with self.assertRaises(TypeError):
self.memory.add_entry("not an entry")
def test_remove_entry(self):
"""Test removing entries"""
entry1 = MockEntry("test-id-1", self.test_timestamp)
entry2 = MockEntry("test-id-2", self.test_timestamp)
self.memory.add_entry(entry1)
self.memory.add_entry(entry2)
self.memory.remove_entry("test-id-1")
self.assertEqual(self.memory.get_entries_count(), 1)
self.assertIsNone(self.memory.get_entry("test-id-1"))
self.assertEqual(self.memory.get_entry("test-id-2"), entry2)
def test_remove_nonexistent_entry(self):
"""Test removing entry that doesn't exist"""
entry = MockEntry("test-id-1", self.test_timestamp)
self.memory.add_entry(entry)
self.memory.remove_entry("nonexistent-id")
self.assertEqual(self.memory.get_entries_count(), 1)
self.assertEqual(self.memory.get_entry("test-id-1"), entry)
def test_update_entries(self):
"""Test updating all entries"""
entries = [
MockEntry(f"test-id-{i}", self.test_timestamp)
for i in range(3)
]
for entry in entries:
self.memory.add_entry(entry)
self.memory.update()
for entry in entries:
self.assertTrue(entry.updated)
def test_generate_context(self):
"""Test generating XML context"""
entry1 = MockEntry("test-id-1", self.test_timestamp)
entry2 = MockEntry("test-id-2", self.test_timestamp)
self.memory.add_entry(entry1)
self.memory.add_entry(entry2)
context = self.memory.generate_context()
self.assertEqual(len(context), 2)
self.assertEqual(context[0].tag, "mock")
self.assertEqual(context[0].get("id"), "test-id-1")
self.assertEqual(context[1].tag, "mock")
self.assertEqual(context[1].get("id"), "test-id-2")
def test_get_entries_by_type(self):
"""Test retrieving entries by type"""
# Create IO buffer for IO entries
io_buffer = WebIOBuffer()
# Add different types of entries
entries = [
ReasoningEntry("test reasoning", "reasoning-1", self.test_timestamp),
ParseErrorEntry("bad content", "error msg", "error-1", self.test_timestamp),
ReadEntry(io_buffer, "read-1", self.test_timestamp),
WriteEntry("test output", io_buffer, "write-1", self.test_timestamp)
]
for entry in entries:
self.memory.add_entry(entry)
# Test filtering by each type
reasoning_entries = self.memory.get_entries_by_type(ReasoningEntry)
self.assertEqual(len(reasoning_entries), 1)
self.assertIsInstance(reasoning_entries[0], ReasoningEntry)
error_entries = self.memory.get_entries_by_type(ParseErrorEntry)
self.assertEqual(len(error_entries), 1)
self.assertIsInstance(error_entries[0], ParseErrorEntry)
read_entries = self.memory.get_entries_by_type(ReadEntry)
self.assertEqual(len(read_entries), 1)
self.assertIsInstance(read_entries[0], ReadEntry)
write_entries = self.memory.get_entries_by_type(WriteEntry)
self.assertEqual(len(write_entries), 1)
self.assertIsInstance(write_entries[0], WriteEntry)
def test_empty_context_generation(self):
"""Test context generation with no entries"""
context = self.memory.generate_context()
self.assertEqual(context, [])
def test_get_nonexistent_entry(self):
"""Test retrieving entry that doesn't exist"""
self.assertIsNone(self.memory.get_entry("nonexistent-id"))
def test_remove_entry_cleanup(self):
"""Test that removing an entry triggers cleanup"""
# Create a background process entry
entry = BackgroundEntry("sleep 10", "test-id", datetime(2024, 10, 31, 12, 0, 0))
self.memory.add_entry(entry)
# Start the process
entry.update()
self.assertIsNotNone(entry.process)
process_pid = entry.process.pid
# Remove entry should trigger cleanup
self.memory.remove_entry("test-id")
# Verify process was terminated
time.sleep(0.1) # Give process time to terminate
with self.assertRaises(ProcessLookupError):
import os
os.kill(process_pid, 0)
def test_cleanup_on_memory_clear(self):
"""Test that clearing memory properly cleans up all entries"""
# Add multiple background processes
processes = []
for i in range(3):
entry = BackgroundEntry(
"sleep 10",
f"id-{i}",
datetime(2024, 10, 31, 12, 0, 0)
)
self.memory.add_entry(entry)
entry.update()
processes.append(entry.process.pid)
# Clear memory
self.memory.clear()
# Verify all processes were terminated
time.sleep(0.1) # Give processes time to terminate
for pid in processes:
with self.assertRaises(ProcessLookupError):
import os
os.kill(pid, 0)
self.assertEqual(self.memory.get_entries_count(), 0)
def test_cleanup_on_memory_deletion(self):
"""Test that deleting memory properly cleans up all entries"""
# Add a background process
entry = BackgroundEntry(
"sleep 10",
"test-id",
datetime(2024, 10, 31, 12, 0, 0)
)
self.memory.add_entry(entry)
entry.update()
process_pid = entry.process.pid
# Delete memory
del self.memory
# Verify process was terminated
time.sleep(0.1) # Give process time to terminate
with self.assertRaises(ProcessLookupError):
import os
os.kill(process_pid, 0)
def test_get_entries(self):
"""Test getting all entries"""
# Add multiple entries
entries = [
ReasoningEntry(f"content {i}", f"id-{i}", self.test_timestamp)
for i in range(3)
]
for entry in entries:
self.memory.add_entry(entry)
# Get all entries
retrieved_entries = self.memory.get_entries()
# Verify count and contents
self.assertEqual(len(retrieved_entries), len(entries))
for entry in entries:
self.assertIn(entry, retrieved_entries)
def test_get_entries_returns_copy(self):
"""Test that get_entries returns a copy of the list"""
# Add an entry
entry = ReasoningEntry("test content", "test-id", self.test_timestamp)
self.memory.add_entry(entry)
# Get entries and modify the returned list
entries = self.memory.get_entries()
entries.clear()
# Verify original memory is unchanged
self.assertEqual(self.memory.get_entries_count(), 1)
self.assertIsNotNone(self.memory.get_entry("test-id"))

117
test/write_entry_test.py Normal file
View File

@@ -0,0 +1,117 @@
import unittest
from datetime import datetime
from sia.web_io_buffer import WebIOBuffer
from sia.write_entry import WriteEntry
class WriteEntryTest(unittest.TestCase):
def setUp(self):
"""Set up test cases with fixed id, timestamp, and IO buffer"""
self.test_id = "test-id-1234"
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
self.io_buffer = WebIOBuffer()
def test_initialization(self):
"""Test entry initialization"""
content = "test message"
entry = WriteEntry(content, self.io_buffer, self.test_id, self.test_timestamp)
self.assertEqual(entry.content, content)
self.assertEqual(entry.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp)
self.assertFalse(entry._written)
self.assertEqual(self.io_buffer.get_stdout(), "")
def test_single_write(self):
"""Test writing content once"""
content = "test message"
entry = WriteEntry(content, self.io_buffer, self.test_id, self.test_timestamp)
# Perform update and verify content was written
entry.update()
self.assertEqual(self.io_buffer.get_stdout(), content)
self.assertTrue(entry._written)
def test_multiple_updates(self):
"""Test that content is only written once even with multiple updates"""
content = "test message"
entry = WriteEntry(content, self.io_buffer, self.test_id, self.test_timestamp)
# Perform multiple updates
entry.update()
initial_output = self.io_buffer.get_stdout()
self.io_buffer.clear_stdout()
entry.update()
entry.update()
# Verify no additional content was written
self.assertEqual(self.io_buffer.get_stdout(), "")
self.assertTrue(entry._written)
self.assertEqual(initial_output, content)
def test_empty_content(self):
"""Test writing empty content"""
entry = WriteEntry("", self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(self.io_buffer.get_stdout(), "")
self.assertTrue(entry._written)
def test_special_characters(self):
"""Test writing content with special characters"""
content = "Special chars: \n\t\r\'\"\\"
entry = WriteEntry(content, self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(self.io_buffer.get_stdout(), content)
def test_large_content(self):
"""Test writing large content"""
content = "x" * 10000
entry = WriteEntry(content, self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(self.io_buffer.get_stdout(), content)
def test_generate_context(self):
"""Test XML context generation"""
content = "test message"
entry = WriteEntry(content, self.io_buffer, self.test_id, self.test_timestamp)
# Generate context before writing
element = entry.generate_context()
# Verify XML structure
self.assertEqual(element.tag, "write_stdout")
self.assertEqual(element.get("id"), self.test_id)
self.assertEqual(element.text, f"<![CDATA[{content}]]>")
# Write content and verify context remains the same
entry.update()
element_after = entry.generate_context()
self.assertEqual(element_after.tag, "write_stdout")
self.assertEqual(element_after.get("id"), self.test_id)
self.assertEqual(element_after.text, f"<![CDATA[{content}]]>")
def test_unicode_content(self):
"""Test writing Unicode content"""
content = "Hello 世界 😊"
entry = WriteEntry(content, self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(self.io_buffer.get_stdout(), content)
def test_multiline_content(self):
"""Test writing multiline content"""
content = """Line 1
Line 2
Line 3"""
entry = WriteEntry(content, self.io_buffer, self.test_id, self.test_timestamp)
entry.update()
self.assertEqual(self.io_buffer.get_stdout(), content)
# Verify XML escaping for multiline content
element = entry.generate_context()
self.assertEqual(element.text, f"<![CDATA[{content}]]>")

116
test/xml_validator_test.py Normal file
View File

@@ -0,0 +1,116 @@
import unittest
from sia.xml_validator import XMLValidator
class XMLValidatorTest(unittest.TestCase):
def setUp(self):
"""Set up test schema"""
self.test_schema = """<?xml version="1.0" encoding="UTF-8"?>
<xs:schema xmlns:xs="http://www.w3.org/2001/XMLSchema">
<xs:element name="test_element">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string">
<xs:attribute name="id" type="xs:string" use="required"/>
<xs:attribute name="count" type="xs:integer"/>
</xs:extension>
</xs:simpleContent>
</xs:complexType>
</xs:element>
<xs:element name="simple_element">
<xs:complexType>
<xs:simpleContent>
<xs:extension base="xs:string"/>
</xs:simpleContent>
</xs:complexType>
</xs:element>
</xs:schema>"""
self.validator = XMLValidator(self.test_schema)
def test_initialization(self):
"""Test validator initialization and root element extraction"""
valid_elements = self.validator.get_valid_root_elements()
self.assertEqual(valid_elements, {'test_element', 'simple_element'})
def test_invalid_schema(self):
"""Test handling invalid schema"""
with self.assertRaises(ValueError):
XMLValidator("invalid schema")
def test_valid_xml(self):
"""Test validation of valid XML"""
xml = '<test_element id="123" count="42">test content</test_element>'
result = self.validator.validate(xml)
self.assertIsNone(result)
# Test without optional attribute
xml = '<test_element id="123">test content</test_element>'
result = self.validator.validate(xml)
self.assertIsNone(result)
# Test simple element
xml = '<simple_element>test content</simple_element>'
result = self.validator.validate(xml)
self.assertIsNone(result)
def test_invalid_root_element(self):
"""Test validation with invalid root element"""
xml = '<invalid_element>test content</invalid_element>'
result = self.validator.validate(xml)
self.assertIsNotNone(result)
self.assertIn("Invalid root element", result)
def test_missing_required_attribute(self):
"""Test validation with missing required attribute"""
xml = '<test_element>test content</test_element>'
result = self.validator.validate(xml)
self.assertIsNotNone(result)
self.assertIn("Missing required attribute", result)
def test_invalid_integer_attribute(self):
"""Test validation with invalid integer attribute"""
xml = '<test_element id="123" count="not_a_number">test content</test_element>'
result = self.validator.validate(xml)
self.assertIsNotNone(result)
self.assertIn("Invalid integer value", result)
def test_unexpected_attribute(self):
"""Test validation with unexpected attribute"""
xml = '<test_element id="123" invalid_attr="value">test content</test_element>'
result = self.validator.validate(xml)
self.assertIsNotNone(result)
self.assertIn("Unexpected attribute", result)
def test_invalid_xml_syntax(self):
"""Test validation with invalid XML syntax"""
xml = '<test_element>unclosed tag'
result = self.validator.validate(xml)
self.assertIsNotNone(result)
self.assertIn("Invalid XML", result)
def test_whitespace_handling(self):
"""Test validation with whitespace in XML"""
xml = """
<test_element id="123">
test content
</test_element>
"""
result = self.validator.validate(xml)
self.assertIsNone(result)
def test_empty_content(self):
"""Test validation with empty element content"""
xml = '<test_element id="123"></test_element>'
result = self.validator.validate(xml)
self.assertIsNone(result)
xml = '<test_element id="123"/>'
result = self.validator.validate(xml)
self.assertIsNone(result)
def test_special_characters(self):
"""Test validation with special characters"""
xml = '<test_element id="123"><![CDATA[Special & < > " \' chars]]></test_element>'
result = self.validator.validate(xml)
self.assertIsNone(result)