Private instance vars, public get properties

This commit is contained in:
2024-11-01 15:45:54 +01:00
parent d80171de15
commit a95c9676b4
18 changed files with 429 additions and 427 deletions

View File

@@ -328,8 +328,8 @@ classDiagram
class Entry { class Entry {
<<abstract>> <<abstract>>
+id: str +id: str readonly
+timestamp: datetime +timestamp: datetime readonly
+Entry(id str, timestamp datetime) +Entry(id str, timestamp datetime)
+update() void* +update() void*
@@ -480,8 +480,8 @@ stateDiagram-v2
classDiagram classDiagram
class Entry { class Entry {
<<abstract>> <<abstract>>
+id: str +id: str readonly
+timestamp: datetime +timestamp: datetime readonly
+Entry(id str, timestamp datetime) +Entry(id str, timestamp datetime)
+update() void* +update() void*
@@ -490,10 +490,10 @@ classDiagram
} }
class SingleShotEntry { class SingleShotEntry {
+script: str +script: str readonly
+stdout: str +stdout: str readonly
+stderr: str +stderr: str readonly
+exit_code: Optional~int~ +exit_code: Optional~int~ readonly
+SingleShotEntry(script str, id str, timestamp datetime) +SingleShotEntry(script str, id str, timestamp datetime)
+update() void +update() void
@@ -501,10 +501,10 @@ classDiagram
} }
class RepeatEntry { class RepeatEntry {
+script: str +script: str readonly
+stdout: str +stdout: str readonly
+stderr: str +stderr: str readonly
+exit_code: Optional~int~ +exit_code: Optional~int~ readonly
+RepeatEntry(script str, id str, timestamp datetime) +RepeatEntry(script str, id str, timestamp datetime)
+update() void +update() void
@@ -512,13 +512,11 @@ classDiagram
} }
class BackgroundEntry { class BackgroundEntry {
+script: str +script: str readonly
+stdout: str +stdout: str readonly
+stderr: str +stderr: str readonly
+process: Optional~Process~ +exit_code: Optional~int~ readonly
-accumulated_stdout: str +pid: Optional~int~ readonly
-accumulated_stderr: str
-exit_code: Optional~int~
+BackgroundEntry(script str, id str, timestamp datetime) +BackgroundEntry(script str, id str, timestamp datetime)
+update() void +update() void
@@ -527,7 +525,7 @@ classDiagram
} }
class ReasoningEntry { class ReasoningEntry {
+content: str +content: str readonly
+ReasoningEntry(content str, id str, timestamp datetime) +ReasoningEntry(content str, id str, timestamp datetime)
+update() void +update() void
@@ -535,8 +533,8 @@ classDiagram
} }
class ParseErrorEntry { class ParseErrorEntry {
+content: str +content: str readonly
+error: str +error: str readonly
+ParseErrorEntry(content str, error str, id str, timestamp datetime) +ParseErrorEntry(content str, error str, id str, timestamp datetime)
+update() void +update() void
@@ -544,8 +542,7 @@ classDiagram
} }
class ReadEntry { class ReadEntry {
+content: str +content: str readonly
+io_buffer: IOBuffer
+ReadEntry(io_buffer IOBuffer, id str, timestamp datetime) +ReadEntry(io_buffer IOBuffer, id str, timestamp datetime)
+update() void +update() void
@@ -553,8 +550,7 @@ classDiagram
} }
class WriteEntry { class WriteEntry {
+content: str +content: str readonly
+io_buffer: IOBuffer
+WriteEntry(content str, io_buffer IOBuffer, id str, timestamp datetime) +WriteEntry(content str, io_buffer IOBuffer, id str, timestamp datetime)
+update() void +update() void

View File

@@ -10,13 +10,11 @@ class BackgroundEntry(Entry):
""" """
Entry type for long-running background processes. Entry type for long-running background processes.
Attributes: Private Attributes:
script: The script/command to execute _script: The script/command to execute
stdout: Captured standard output since last update _stdout: Captured standard output
stderr: Captured standard error since last update _stderr: Captured standard error
process: The running subprocess.Popen instance _process: The running subprocess.Popen instance
_accumulated_stdout: Total stdout collected so far
_accumulated_stderr: Total stderr collected so far
_exit_code: Exit code when process completes _exit_code: Exit code when process completes
""" """
@@ -30,58 +28,73 @@ class BackgroundEntry(Entry):
timestamp: Creation timestamp for this entry timestamp: Creation timestamp for this entry
""" """
super().__init__(id, timestamp) super().__init__(id, timestamp)
self.script = script self._script = script
self.stdout = "" self._stdout = ""
self.stderr = "" self._stderr = ""
self.process: Optional[subprocess.Popen] = None self._process: Optional[subprocess.Popen] = None
self._accumulated_stdout = ""
self._accumulated_stderr = ""
self._exit_code: Optional[int] = None self._exit_code: Optional[int] = None
@property
def script(self) -> str:
"""Get the script/command being executed."""
return self._script
@property
def stdout(self) -> str:
"""Get the captured standard output."""
return self._stdout
@property
def stderr(self) -> str:
"""Get the captured standard error."""
return self._stderr
@property
def exit_code(self) -> Optional[int]:
"""Get the exit code of the process (None if still running)."""
return self._exit_code
@property
def pid(self) -> Optional[int]:
"""Get the process ID (None if not running)."""
return self._process.pid if self._process is not None else None
def cleanup(self) -> None: def cleanup(self) -> None:
""" """
Clean up the background process if it's still running. Clean up the background process if it's still running.
Ensures process is terminated and file handles are closed. Ensures process is terminated and file handles are closed.
""" """
self._cleanup_process() if self._process is not None:
def _cleanup_process(self):
"""Clean up process and file handles"""
if self.process is not None:
try: try:
# Close file handles if they're open # Close file handles if they're open
if self.process.stdout: if self._process.stdout:
self.process.stdout.close() self._process.stdout.close()
if self.process.stderr: if self._process.stderr:
self.process.stderr.close() self._process.stderr.close()
# Terminate process if it's still running # Terminate process if it's still running
if self.process.poll() is None: if self._process.poll() is None:
self.process.terminate() self._process.terminate()
try: try:
self.process.wait(timeout=1.0) self._process.wait(timeout=1.0)
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
self.process.kill() self._process.kill()
self.process.wait() self._process.wait()
except: except:
pass # Ignore cleanup errors pass # Ignore cleanup errors
finally: finally:
self.process = None self._process = None
def update(self) -> None: def update(self) -> None:
""" """
Start the process if not running and collect any new output. Start the process if not running and collect any new output.
Updates stdout and stderr with any new output since last update. Updates stdout and stderr with any new output.
""" """
try: try:
# Reset current output buffers
self.stdout = ""
self.stderr = ""
# Start process if not running # Start process if not running
if self.process is None and self._exit_code is None: if self._process is None and self._exit_code is None:
self.process = subprocess.Popen( self._process = subprocess.Popen(
self.script, self._script,
shell=True, shell=True,
stdout=subprocess.PIPE, stdout=subprocess.PIPE,
stderr=subprocess.PIPE, stderr=subprocess.PIPE,
@@ -90,57 +103,52 @@ class BackgroundEntry(Entry):
) )
return # Return after starting to allow process to generate output return # Return after starting to allow process to generate output
if self.process is None: if self._process is None:
return # Process already completed return # Process already completed
# Check if process has finished # Check if process has finished
exit_code = self.process.poll() exit_code = self._process.poll()
if exit_code is not None: if exit_code is not None:
# Process has terminated, collect remaining output # Process has terminated, collect remaining output
try: try:
remaining_out, remaining_err = self.process.communicate(timeout=0.1) remaining_out, remaining_err = self._process.communicate(timeout=0.1)
self.stdout = remaining_out self._stdout += remaining_out
self.stderr = remaining_err self._stderr += remaining_err
self._accumulated_stdout += remaining_out
self._accumulated_stderr += remaining_err
except subprocess.TimeoutExpired: except subprocess.TimeoutExpired:
pass # Process didn't finish communicating, try again next update pass # Process didn't finish communicating, try again next update
self._exit_code = exit_code self._exit_code = exit_code
self._cleanup_process() self.cleanup()
return return
# Read stdout if available # Read stdout if available
if self.process.stdout: if self._process.stdout:
while True: while True:
try: try:
line = self.process.stdout.readline() line = self._process.stdout.readline()
if not line: if not line:
break break
self.stdout += line self._stdout += line
self._accumulated_stdout += line
except: except:
break break
# Read stderr if available # Read stderr if available
if self.process.stderr: if self._process.stderr:
while True: while True:
try: try:
line = self.process.stderr.readline() line = self._process.stderr.readline()
if not line: if not line:
break break
self.stderr += line self._stderr += line
self._accumulated_stderr += line
except: except:
break break
except Exception as e: except Exception as e:
# Handle any errors # Handle any errors
error_msg = f"Error handling background process: {str(e)}" error_msg = f"Error handling background process: {str(e)}"
self.stderr = error_msg self._stderr += f"\n{error_msg}"
self._accumulated_stderr += f"\n{error_msg}"
self._exit_code = -1 self._exit_code = -1
self._cleanup_process() self.cleanup()
def generate_context(self) -> ET.Element: def generate_context(self) -> ET.Element:
""" """
@@ -150,26 +158,22 @@ class BackgroundEntry(Entry):
ET.Element: XML element containing the entry's data ET.Element: XML element containing the entry's data
""" """
# Create root element with appropriate status # Create root element with appropriate status
element = ET.Element("background", {"id": self.id}) element = ET.Element("background", {"id": self._id})
if self.process is not None: if self._process is not None:
element.set("pid", str(self.process.pid)) element.set("pid", str(self._process.pid))
elif self._exit_code is not None: elif self._exit_code is not None:
element.set("exit_code", str(self._exit_code)) element.set("exit_code", str(self._exit_code))
# Add script as CDATA or escaped text # Add script as CDATA or escaped text
element.text = escape_text_for_xml(self.script) element.text = escape_text_for_xml(self._script)
# Add stdout element with accumulated output # Add stdout element with accumulated output
stdout_elem = ET.SubElement(element, "stdout") stdout_elem = ET.SubElement(element, "stdout")
stdout_elem.text = escape_text_for_xml(self._accumulated_stdout) stdout_elem.text = escape_text_for_xml(self._stdout)
# Add stderr element with accumulated output # Add stderr element with accumulated output
stderr_elem = ET.SubElement(element, "stderr") stderr_elem = ET.SubElement(element, "stderr")
stderr_elem.text = escape_text_for_xml(self._accumulated_stderr) stderr_elem.text = escape_text_for_xml(self._stderr)
return element return element
def __del__(self):
"""Ensure process is terminated and file handles are closed when entry is destroyed"""
self._cleanup_process()

View File

@@ -5,10 +5,6 @@ import xml.etree.ElementTree as ET
class Entry(ABC): class Entry(ABC):
""" """
Abstract base class for all entry types in the working memory. Abstract base class for all entry types in the working memory.
Attributes:
id: Unique identifier for the entry
timestamp: When the entry was created
""" """
def __init__(self, id: str, timestamp: datetime): def __init__(self, id: str, timestamp: datetime):
@@ -19,8 +15,18 @@ class Entry(ABC):
id: Unique identifier for this entry id: Unique identifier for this entry
timestamp: Creation timestamp for this entry timestamp: Creation timestamp for this entry
""" """
self.id = id self._id = id
self.timestamp = timestamp self._timestamp = timestamp
@property
def id(self) -> str:
"""Get entry's unique identifier."""
return self._id
@property
def timestamp(self) -> datetime:
"""Get entry's creation timestamp."""
return self._timestamp
@abstractmethod @abstractmethod
def update(self) -> None: def update(self) -> None:

View File

@@ -7,10 +7,6 @@ from .util import escape_text_for_xml
class ParseErrorEntry(Entry): class ParseErrorEntry(Entry):
""" """
Entry type for parse and validation errors. Entry type for parse and validation errors.
Attributes:
content: The original content that failed to parse
error: The error message describing what went wrong
""" """
def __init__(self, content: str, error: str, id: str, timestamp: datetime): def __init__(self, content: str, error: str, id: str, timestamp: datetime):
@@ -24,11 +20,20 @@ class ParseErrorEntry(Entry):
timestamp: Creation timestamp for this entry timestamp: Creation timestamp for this entry
""" """
super().__init__(id, timestamp) super().__init__(id, timestamp)
self.content = content self._content = content
self.error = error self._error = error
@property
def content(self) -> str:
"""Get the original content that failed to parse."""
return self._content
@property
def error(self) -> str:
"""Get the error message describing the failure."""
return self._error
def update(self) -> None: def update(self) -> None:
"""No update needed for parse error entries."""
pass pass
def generate_context(self) -> ET.Element: def generate_context(self) -> ET.Element:
@@ -39,14 +44,14 @@ class ParseErrorEntry(Entry):
ET.Element: XML element containing the entry's data ET.Element: XML element containing the entry's data
""" """
# Create root element # Create root element
element = ET.Element("parse_error", {"id": self.id}) element = ET.Element("parse_error", {"id": self._id})
# Add error subelement # Add error subelement
error_elem = ET.SubElement(element, "error") error_elem = ET.SubElement(element, "error")
error_elem.text = escape_text_for_xml(self.error) error_elem.text = escape_text_for_xml(self._error)
# Add content subelement # Add content subelement
content_elem = ET.SubElement(element, "content") content_elem = ET.SubElement(element, "content")
content_elem.text = escape_text_for_xml(self.content) content_elem.text = escape_text_for_xml(self._content)
return element return element

View File

@@ -9,8 +9,8 @@ class ReadEntry(Entry):
Entry type for reading content from standard input. Entry type for reading content from standard input.
Attributes: Attributes:
content: Content read from stdin, empty until first update _content: Content read from stdin, empty until first update
io_buffer: Buffer to use for IO operations _io_buffer: Buffer to use for IO operations
_read: Whether content has been read _read: Whether content has been read
""" """
@@ -24,17 +24,24 @@ class ReadEntry(Entry):
timestamp: Creation timestamp for this entry timestamp: Creation timestamp for this entry
""" """
super().__init__(id, timestamp) super().__init__(id, timestamp)
self.content = "" self._content = ""
self.io_buffer = io_buffer self._io_buffer = io_buffer
self._read = False self._read = False
@property
def content(self) -> str:
"""
Get the content read from stdin.
"""
return self._content
def update(self) -> None: def update(self) -> None:
""" """
Read from stdin if not already read. Read from stdin if not already read.
Uses the provided IO buffer for the actual read operation. Uses the provided IO buffer for the actual read operation.
""" """
if not self._read: if not self._read:
self.content = self.io_buffer.read() self._content = self._io_buffer.read()
self._read = True self._read = True
def generate_context(self) -> ET.Element: def generate_context(self) -> ET.Element:
@@ -45,10 +52,10 @@ class ReadEntry(Entry):
ET.Element: XML element containing the entry's data ET.Element: XML element containing the entry's data
""" """
# Create root element # Create root element
element = ET.Element("read_stdin", {"id": self.id}) element = ET.Element("read_stdin", {"id": self._id})
# Add content as CDATA or escaped text if content has been read # Add content as CDATA or escaped text if content has been read
if self._read: if self._read:
element.text = escape_text_for_xml(self.content) element.text = escape_text_for_xml(self._content)
return element return element

View File

@@ -9,7 +9,7 @@ class ReasoningEntry(Entry):
Entry type for agent reasoning steps. Entry type for agent reasoning steps.
Attributes: Attributes:
content: The reasoning text _content: The reasoning text
""" """
def __init__(self, content: str, id: str, timestamp: datetime): def __init__(self, content: str, id: str, timestamp: datetime):
@@ -22,7 +22,14 @@ class ReasoningEntry(Entry):
timestamp: Creation timestamp for this entry timestamp: Creation timestamp for this entry
""" """
super().__init__(id, timestamp) super().__init__(id, timestamp)
self.content = content self._content = content
@property
def content(self) -> str:
"""
Get the reasoning text.
"""
return self._content
def update(self) -> None: def update(self) -> None:
"""No update needed for reasoning entries.""" """No update needed for reasoning entries."""
@@ -36,9 +43,9 @@ class ReasoningEntry(Entry):
ET.Element: XML element containing the entry's data ET.Element: XML element containing the entry's data
""" """
# Create root element # Create root element
element = ET.Element("reasoning", {"id": self.id}) element = ET.Element("reasoning", {"id": self._id})
# Add content as CDATA or escaped text # Add content as CDATA or escaped text
element.text = escape_text_for_xml(self.content) element.text = escape_text_for_xml(self._content)
return element return element

View File

@@ -9,12 +9,6 @@ from .util import escape_text_for_xml
class RepeatEntry(Entry): class RepeatEntry(Entry):
""" """
Entry type for scripts that are executed on every update. Entry type for scripts that are executed on every update.
Attributes:
script: The script/command to execute
stdout: Captured standard output from most recent execution
stderr: Captured standard error from most recent execution
exit_code: Process exit code from most recent execution
""" """
def __init__(self, script: str, id: str, timestamp: datetime): def __init__(self, script: str, id: str, timestamp: datetime):
@@ -27,10 +21,30 @@ class RepeatEntry(Entry):
timestamp: Creation timestamp for this entry timestamp: Creation timestamp for this entry
""" """
super().__init__(id, timestamp) super().__init__(id, timestamp)
self.script = script self._script = script
self.stdout = "" self._stdout = ""
self.stderr = "" self._stderr = ""
self.exit_code: Optional[int] = None self._exit_code: Optional[int] = None
@property
def script(self) -> str:
"""Get the script/command being executed."""
return self._script
@property
def stdout(self) -> str:
"""Get the captured standard output."""
return self._stdout
@property
def stderr(self) -> str:
"""Get the captured standard error."""
return self._stderr
@property
def exit_code(self) -> Optional[int]:
"""Get the exit code of the process (None if still running)."""
return self._exit_code
def update(self) -> None: def update(self) -> None:
""" """
@@ -40,28 +54,28 @@ class RepeatEntry(Entry):
try: try:
# Run script and capture output # Run script and capture output
process = subprocess.run( process = subprocess.run(
self.script, self._script,
shell=True, shell=True,
capture_output=True, capture_output=True,
text=True text=True
) )
# Store results # Store results
self.stdout = process.stdout self._stdout = process.stdout
self.stderr = process.stderr self._stderr = process.stderr
self.exit_code = process.returncode self._exit_code = process.returncode
except subprocess.SubprocessError as e: except subprocess.SubprocessError as e:
# Handle subprocess errors by capturing the error message # Handle subprocess errors by capturing the error message
self.stdout = "" self._stdout = ""
self.stderr = str(e) self._stderr = str(e)
self.exit_code = -1 self._exit_code = -1
except Exception as e: except Exception as e:
# Handle any other errors # Handle any other errors
self.stdout = "" self._stdout = ""
self.stderr = f"Error executing script: {str(e)}" self._stderr = f"Error executing script: {str(e)}"
self.exit_code = -1 self._exit_code = -1
def generate_context(self) -> ET.Element: def generate_context(self) -> ET.Element:
""" """
@@ -73,7 +87,7 @@ class RepeatEntry(Entry):
# Create root element # Create root element
element = ET.Element("repeat", { element = ET.Element("repeat", {
"id": self.id, "id": self.id,
"exit_code": str(self.exit_code) if self.exit_code is not None else "-1" "exit_code": str(self._exit_code) if self._exit_code is not None else "-1"
}) })
# Add script as CDATA or escaped text # Add script as CDATA or escaped text
@@ -81,10 +95,10 @@ class RepeatEntry(Entry):
# Add stdout element # Add stdout element
stdout_elem = ET.SubElement(element, "stdout") stdout_elem = ET.SubElement(element, "stdout")
stdout_elem.text = escape_text_for_xml(self.stdout) stdout_elem.text = escape_text_for_xml(self._stdout)
# Add stderr element # Add stderr element
stderr_elem = ET.SubElement(element, "stderr") stderr_elem = ET.SubElement(element, "stderr")
stderr_elem.text = escape_text_for_xml(self.stderr) stderr_elem.text = escape_text_for_xml(self._stderr)
return element return element

View File

@@ -28,12 +28,32 @@ class SingleShotEntry(Entry):
timestamp: Creation timestamp for this entry timestamp: Creation timestamp for this entry
""" """
super().__init__(id, timestamp) super().__init__(id, timestamp)
self.script = script self._script = script
self.stdout = "" self._stdout = ""
self.stderr = "" self._stderr = ""
self.exit_code: Optional[int] = None self._exit_code: Optional[int] = None
self._executed = False self._executed = False
@property
def script(self) -> str:
"""Get the script/command being executed."""
return self._script
@property
def stdout(self) -> str:
"""Get the captured standard output."""
return self._stdout
@property
def stderr(self) -> str:
"""Get the captured standard error."""
return self._stderr
@property
def exit_code(self) -> Optional[int]:
"""Get the exit code of the process (None if still running)."""
return self._exit_code
def update(self) -> None: def update(self) -> None:
""" """
Execute the script if not already executed. Execute the script if not already executed.
@@ -52,21 +72,21 @@ class SingleShotEntry(Entry):
) )
# Store results # Store results
self.stdout = process.stdout self._stdout = process.stdout
self.stderr = process.stderr self._stderr = process.stderr
self.exit_code = process.returncode self._exit_code = process.returncode
except subprocess.SubprocessError as e: except subprocess.SubprocessError as e:
# Handle subprocess errors by capturing the error message # Handle subprocess errors by capturing the error message
self.stdout = "" self._stdout = ""
self.stderr = str(e) self._stderr = str(e)
self.exit_code = -1 self._exit_code = -1
except Exception as e: except Exception as e:
# Handle any other errors # Handle any other errors
self.stdout = "" self._stdout = ""
self.stderr = f"Error executing script: {str(e)}" self._stderr = f"Error executing script: {str(e)}"
self.exit_code = -1 self._exit_code = -1
self._executed = True self._executed = True
@@ -89,13 +109,13 @@ class SingleShotEntry(Entry):
if self._executed: if self._executed:
# Add stdout element if there is output # Add stdout element if there is output
stdout_elem = ET.SubElement(element, "stdout") stdout_elem = ET.SubElement(element, "stdout")
stdout_elem.text = escape_text_for_xml(self.stdout) stdout_elem.text = escape_text_for_xml(self._stdout)
# Add stderr element if there are errors # Add stderr element if there are errors
stderr_elem = ET.SubElement(element, "stderr") stderr_elem = ET.SubElement(element, "stderr")
stderr_elem.text = escape_text_for_xml(self.stderr) stderr_elem.text = escape_text_for_xml(self._stderr)
# Add exit code attribute after execution # Add exit code attribute after execution
element.set("exit_code", str(self.exit_code)) element.set("exit_code", str(self._exit_code))
return element return element

View File

@@ -2,8 +2,7 @@ import unittest
from datetime import datetime from datetime import datetime
import os import os
import time import time
import signal from typing import Optional
import platform
from sia.background_entry import BackgroundEntry from sia.background_entry import BackgroundEntry
@@ -19,7 +18,7 @@ class BackgroundEntryTest(unittest.TestCase):
self.cleanup_files() self.cleanup_files()
def tearDown(self): def tearDown(self):
"""Clean up test files and ensure no processes are left running""" """Clean up test files"""
self.cleanup_files() self.cleanup_files()
def cleanup_files(self): def cleanup_files(self):
@@ -28,52 +27,44 @@ class BackgroundEntryTest(unittest.TestCase):
if os.path.exists(filename): if os.path.exists(filename):
os.remove(filename) os.remove(filename)
def wait_for_process_start(self, entry, timeout=1.0): def wait_for_condition(self, entry: BackgroundEntry, condition: callable, timeout: float = 1.0) -> bool:
"""Wait for process to start, with timeout""" """Wait for a condition on the entry context to be met"""
start_time = time.time() start_time = time.time()
while entry.process is None and time.time() - start_time < timeout: while time.time() - start_time < timeout:
entry.update() entry.update()
context = entry.generate_context()
if condition(context):
return True
time.sleep(0.1) time.sleep(0.1)
self.assertIsNotNone(entry.process, "Process failed to start") return False
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): def test_initialization(self):
"""Test entry initialization""" """Test entry initialization"""
script = "echo test" entry = BackgroundEntry("echo test", self.test_id, self.test_timestamp)
entry = BackgroundEntry(script, self.test_id, self.test_timestamp) context = entry.generate_context()
self.assertEqual(entry.script, script) # Initial context should have script but no pid or exit_code
self.assertEqual(entry.stdout, "") self.assertEqual(context.text, "<![CDATA[echo test]]>")
self.assertEqual(entry.stderr, "") self.assertIsNone(context.get("pid"))
self.assertIsNone(entry.process) self.assertIsNone(context.get("exit_code"))
self.assertEqual(entry.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp)
def test_short_running_process(self): def test_short_running_process(self):
"""Test execution of a quick process""" """Test execution of a quick process"""
script = "echo 'test'" entry = BackgroundEntry("echo 'test'", self.test_id, self.test_timestamp)
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Start and wait for completion # Wait for process to complete
entry.update() def is_complete(ctx):
self.wait_for_process_start(entry) return ctx.get("exit_code") == "0"
self.wait_for_process_exit(entry)
self.assertTrue(self.wait_for_condition(entry, is_complete))
# Verify output # Verify output
self.assertEqual(entry._accumulated_stdout.strip(), "test") context = entry.generate_context()
self.assertEqual(entry._accumulated_stderr, "") self.assertTrue("<![CDATA[test" in context.find("stdout").text)
self.assertEqual(entry._exit_code, 0) self.assertEqual(context.find("stderr").text, "<![CDATA[]]>")
def test_continuous_output(self): def test_continuous_output(self):
"""Test process that generates continuous output""" """Test process that generates continuous output"""
# Create a Python script that counts to 3 with minimal delay
script = ( script = (
'python3 -c "' 'python3 -c "'
'import sys\n' 'import sys\n'
@@ -85,143 +76,83 @@ class BackgroundEntryTest(unittest.TestCase):
entry = BackgroundEntry(script, self.test_id, self.test_timestamp) entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Start process and wait for completion # Wait for process to complete
entry.update() def has_all_output(ctx):
self.wait_for_process_start(entry) stdout = ctx.find("stdout").text
self.wait_for_process_exit(entry, timeout=2.0) return all(f"count {i}" in stdout for i in range(1, 4))
# One more update to ensure we get all output self.assertTrue(self.wait_for_condition(entry, has_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): def test_process_failure(self):
"""Test handling of process failures""" """Test handling of process failures"""
entry = BackgroundEntry("nonexistentcommand", self.test_id, self.test_timestamp) entry = BackgroundEntry("nonexistentcommand", self.test_id, self.test_timestamp)
# Start and wait for failure # Wait for process to fail
entry.update() def has_failed(ctx):
self.wait_for_process_exit(entry) return ctx.get("exit_code") is not None and ctx.get("exit_code") != "0"
# Verify error was captured self.assertTrue(self.wait_for_condition(entry, has_failed))
self.assertTrue(len(entry._accumulated_stderr) > 0)
self.assertEqual(entry._accumulated_stdout, "")
self.assertNotEqual(entry._exit_code, 0)
def test_generate_context_running(self): # Verify error captured
"""Test XML context generation for running process""" context = entry.generate_context()
script = "sleep 1" self.assertTrue(len(context.find("stderr").text) > 0)
self.assertEqual(context.find("stdout").text, "<![CDATA[]]>")
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): def test_cleanup_running_process(self):
"""Test cleanup of running process""" """Test cleanup of running process"""
script = "sleep 10" entry = BackgroundEntry("sleep 10", self.test_id, self.test_timestamp)
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Start the process # Wait for process to start
entry.update() def has_started(ctx):
self.assertIsNotNone(entry.process) return ctx.get("pid") is not None
process_pid = entry.process.pid
# Cleanup should terminate process self.assertTrue(self.wait_for_condition(entry, has_started))
# Get PID before cleanup
context = entry.generate_context()
pid = int(context.get("pid"))
# Clean up and verify process terminated
entry.cleanup() entry.cleanup()
# Verify process was terminated # Process should no longer exist
self.assertIsNone(entry.process)
time.sleep(0.1) # Give process time to terminate time.sleep(0.1) # Give process time to terminate
with self.assertRaises(ProcessLookupError): with self.assertRaises(ProcessLookupError):
import os os.kill(pid, 0)
os.kill(process_pid, 0)
def test_cleanup_completed_process(self): def test_cleanup_completed_process(self):
"""Test cleanup of already completed process""" """Test cleanup of already completed process"""
script = "echo 'test'" entry = BackgroundEntry("echo test", self.test_id, self.test_timestamp)
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Run and wait for completion # Wait for completion
entry.update() def is_complete(ctx):
self.wait_for_process_exit(entry) return ctx.get("exit_code") == "0"
# Cleanup should be safe for completed process self.assertTrue(self.wait_for_condition(entry, is_complete))
# Cleanup should be safe
entry.cleanup() entry.cleanup()
self.assertIsNone(entry.process)
def test_multiple_cleanup_calls(self): def test_multiple_cleanup_calls(self):
"""Test that multiple cleanup calls are safe""" """Test that multiple cleanup calls are safe"""
script = "sleep 10" entry = BackgroundEntry("sleep 10", self.test_id, self.test_timestamp)
entry = BackgroundEntry(script, self.test_id, self.test_timestamp)
# Start process # Wait for process to start
entry.update() def has_started(ctx):
self.assertIsNotNone(entry.process) return ctx.get("pid") is not None
self.assertTrue(self.wait_for_condition(entry, has_started))
# Get PID before cleanup
context = entry.generate_context()
pid = int(context.get("pid"))
# Multiple cleanups should be safe # Multiple cleanups should be safe
entry.cleanup() entry.cleanup()
entry.cleanup() entry.cleanup()
entry.cleanup() entry.cleanup()
self.assertIsNone(entry.process) # Process should no longer exist
time.sleep(0.1)
with self.assertRaises(ProcessLookupError):
os.kill(pid, 0)

View File

@@ -1,12 +1,15 @@
import unittest
from datetime import datetime from datetime import datetime
import os
import time import time
import unittest
from sia.working_memory import WorkingMemory from sia.working_memory import WorkingMemory
from sia.delete_command import DeleteCommand from sia.delete_command import DeleteCommand
from sia.reasoning_entry import ReasoningEntry from sia.reasoning_entry import ReasoningEntry
from sia.background_entry import BackgroundEntry from sia.background_entry import BackgroundEntry
class DeleteCommandTest(unittest.TestCase): class DeleteCommandTest(unittest.TestCase):
def setUp(self): def setUp(self):
"""Set up test cases with a fresh working memory""" """Set up test cases with a fresh working memory"""
@@ -14,6 +17,44 @@ class DeleteCommandTest(unittest.TestCase):
self.test_id = "test-id-1234" self.test_id = "test-id-1234"
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0) self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
def wait_for_process_start(self, entry: BackgroundEntry, timeout=1.0):
"""Wait for process to start, checking context pid"""
start_time = time.time()
while time.time() - start_time < timeout:
entry.update()
context = entry.generate_context()
if context.get("pid") is not None:
return True
time.sleep(0.1)
return False
def test_delete_running_background_entry(self):
"""Test deleting a background entry that is still running"""
# Create and start background process
entry = BackgroundEntry("sleep 10", "test-id", self.test_timestamp)
self.memory.add_entry(entry)
# Wait for process to start
self.assertTrue(self.wait_for_process_start(entry))
# Get PID before deletion
context = entry.generate_context()
pid = int(context.get("pid"))
# Delete entry and verify process terminated
command = DeleteCommand(entry.id)
result = command.execute(self.memory)
self.assertTrue(result.success)
self.assertFalse(result.should_stop)
self.assertEqual(result.message, "")
self.assertEqual(self.memory.get_entries_count(), 0)
# Verify process was terminated
time.sleep(0.1) # Give process time to terminate
with self.assertRaises(ProcessLookupError):
os.kill(pid, 0)
def test_delete_existing_entry(self): def test_delete_existing_entry(self):
"""Test deleting an existing entry""" """Test deleting an existing entry"""
# Add test entry # Add test entry
@@ -30,31 +71,6 @@ class DeleteCommandTest(unittest.TestCase):
self.assertEqual(result.message, "") self.assertEqual(result.message, "")
self.assertEqual(self.memory.get_entries_count(), 0) 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): def test_delete_nonexistent_entry(self):
"""Test attempting to delete a nonexistent entry""" """Test attempting to delete a nonexistent entry"""
command = DeleteCommand("nonexistent-id") command = DeleteCommand("nonexistent-id")

25
test/llm_engine_test.py Normal file
View File

@@ -0,0 +1,25 @@
import unittest
from itertools import tee
from . import test_data
from sia.llm_engine import LlmEngine
class LlmEngineTest(unittest.TestCase):
def setUp(self):
self.model_path = "/root/model"
def test_initialization(self):
llm_engine = LlmEngine(self.model_path)
self.assertIsInstance(llm_engine, LlmEngine)
def test_infer(self):
main_context = "This is a test"
llm_engine = LlmEngine(self.model_path)
tokens = llm_engine.infer(test_data.echo_system_prompt, main_context)
print_tokens, result_tokens = tee(tokens)
for token in print_tokens:
print(token, end="", flush=True)
result = ''.join(result_tokens)
self.assertEqual(result, f"{main_context}<test_tag>{main_context}</test_tag>")

View File

@@ -15,8 +15,6 @@ class ParseErrorEntryTest(unittest.TestCase):
error = "parsing failed" error = "parsing failed"
entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp) 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.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp) self.assertEqual(entry.timestamp, self.test_timestamp)
@@ -27,15 +25,13 @@ class ParseErrorEntryTest(unittest.TestCase):
entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp) entry = ParseErrorEntry(content, error, self.test_id, self.test_timestamp)
# Store initial state # Store initial state
initial_content = entry.content initial_context = str(entry.generate_context())
initial_error = entry.error
# Perform update # Perform update
entry.update() entry.update()
# Verify state hasn't changed # Verify state hasn't changed
self.assertEqual(entry.content, initial_content) self.assertEqual(str(entry.generate_context()), initial_context)
self.assertEqual(entry.error, initial_error)
def test_generate_context(self): def test_generate_context(self):
"""Test XML context generation""" """Test XML context generation"""
@@ -76,13 +72,3 @@ class ParseErrorEntryTest(unittest.TestCase):
element = entry.generate_context() element = entry.generate_context()
self.assertEqual(element.find("content").text, "<![CDATA[]]>") self.assertEqual(element.find("content").text, "<![CDATA[]]>")
self.assertEqual(element.find("error").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}]]>")

View File

@@ -18,7 +18,6 @@ class ReadEntryTest(unittest.TestCase):
self.assertEqual(entry.content, "") self.assertEqual(entry.content, "")
self.assertEqual(entry.id, self.test_id) self.assertEqual(entry.id, self.test_id)
self.assertEqual(entry.timestamp, self.test_timestamp) self.assertEqual(entry.timestamp, self.test_timestamp)
self.assertFalse(entry._read)
def test_single_read(self): def test_single_read(self):
"""Test reading content once""" """Test reading content once"""
@@ -28,10 +27,8 @@ class ReadEntryTest(unittest.TestCase):
entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp) entry = ReadEntry(self.io_buffer, self.test_id, self.test_timestamp)
entry.update() entry.update()
# Verify content was read
self.assertEqual(entry.content, test_input) self.assertEqual(entry.content, test_input)
self.assertTrue(entry._read) self.assertEqual(self.io_buffer.buffer_length(), 0)
self.assertEqual(self.io_buffer.buffer_length(), 0) # Buffer should be cleared
def test_multiple_updates(self): def test_multiple_updates(self):
"""Test that content is only read once even with multiple updates""" """Test that content is only read once even with multiple updates"""
@@ -42,14 +39,11 @@ class ReadEntryTest(unittest.TestCase):
entry.update() entry.update()
initial_content = entry.content initial_content = entry.content
# Add more input and update again
self.io_buffer.append_stdin("additional input") self.io_buffer.append_stdin("additional input")
entry.update() entry.update()
entry.update() entry.update()
# Verify content hasn't changed after first read
self.assertEqual(entry.content, initial_content) self.assertEqual(entry.content, initial_content)
self.assertTrue(entry._read)
def test_empty_input(self): def test_empty_input(self):
"""Test reading when no input is available""" """Test reading when no input is available"""
@@ -57,7 +51,6 @@ class ReadEntryTest(unittest.TestCase):
entry.update() entry.update()
self.assertEqual(entry.content, "") self.assertEqual(entry.content, "")
self.assertTrue(entry._read)
def test_special_characters(self): def test_special_characters(self):
"""Test reading content with special characters""" """Test reading content with special characters"""

View File

@@ -24,13 +24,13 @@ class ReasoningEntryTest(unittest.TestCase):
entry = ReasoningEntry(content, self.test_id, self.test_timestamp) entry = ReasoningEntry(content, self.test_id, self.test_timestamp)
# Store initial state # Store initial state
initial_content = entry.content initial_content = entry.generate_context().text
# Perform update # Perform update
entry.update() entry.update()
# Verify state hasn't changed # Verify state hasn't changed
self.assertEqual(entry.content, initial_content) self.assertEqual(entry.generate_context().text, initial_content)
def test_generate_context(self): def test_generate_context(self):
"""Test XML context generation""" """Test XML context generation"""

View File

@@ -146,19 +146,3 @@ class RepeatEntryTest(unittest.TestCase):
unique_outputs = set(outputs) unique_outputs = set(outputs)
self.assertEqual(len(unique_outputs), 3) self.assertEqual(len(unique_outputs), 3)
self.assertEqual(outputs, ['Output 1', 'Output 2', 'Output 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

@@ -120,7 +120,6 @@ class ResponseParserTest(unittest.TestCase):
result = self.parser.parse(xml) result = self.parser.parse(xml)
self.assertIsInstance(result, ReadEntry) self.assertIsInstance(result, ReadEntry)
self.assertEqual(result.io_buffer, self.io_buffer)
def test_write_stdout_entry(self): def test_write_stdout_entry(self):
"""Test parsing write stdout entry""" """Test parsing write stdout entry"""

View File

@@ -1,10 +1,10 @@
import unittest
from datetime import datetime from datetime import datetime
import os
import time import time
import unittest
from sia.working_memory import WorkingMemory from sia.working_memory import WorkingMemory
from sia.stop_command import StopCommand from sia.stop_command import StopCommand
from sia.reasoning_entry import ReasoningEntry
from sia.background_entry import BackgroundEntry from sia.background_entry import BackgroundEntry
class StopCommandTest(unittest.TestCase): class StopCommandTest(unittest.TestCase):
@@ -13,33 +13,37 @@ class StopCommandTest(unittest.TestCase):
self.memory = WorkingMemory() self.memory = WorkingMemory()
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0) self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
def wait_for_process_start(self, entry: BackgroundEntry, timeout=1.0):
"""Wait for process to start, checking context pid"""
start_time = time.time()
while time.time() - start_time < timeout:
entry.update()
context = entry.generate_context()
if context.get("pid") is not None:
return True
time.sleep(0.1)
return False
def test_stop_command_cleanup(self): def test_stop_command_cleanup(self):
"""Test stop command cleans up and removes all entries""" """Test stop command cleans up and removes all entries"""
# Add a mix of entry types including a background process # Add background process
entries = [ entry = BackgroundEntry("sleep 10", "test-id", self.test_timestamp)
ReasoningEntry("test content", "id-1", self.test_timestamp), self.memory.add_entry(entry)
BackgroundEntry("sleep 10", "id-2", self.test_timestamp)
]
for entry in entries: # Wait for process to start and get PID
self.memory.add_entry(entry) self.assertTrue(self.wait_for_process_start(entry))
context = entry.generate_context()
# Start the background process pid = int(context.get("pid"))
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 # Execute stop command
command = StopCommand() command = StopCommand()
result = command.execute(self.memory) result = command.execute(self.memory)
# Verify all entries removed # Verify entries removed and process terminated
self.assertTrue(result.should_stop) self.assertTrue(result.should_stop)
self.assertEqual(self.memory.get_entries_count(), 0) self.assertEqual(self.memory.get_entries_count(), 0)
# Verify background process was terminated # Verify process was terminated
time.sleep(0.1) # Give process time to terminate time.sleep(0.1) # Give process time to terminate
with self.assertRaises(ProcessLookupError): with self.assertRaises(ProcessLookupError):
import os os.kill(pid, 0)
os.kill(process_pid, 0)

View File

@@ -1,4 +1,5 @@
from datetime import datetime from datetime import datetime
import os
import time import time
import unittest import unittest
import xml.etree.ElementTree as ET import xml.etree.ElementTree as ET
@@ -12,6 +13,7 @@ from sia.web_io_buffer import WebIOBuffer
from sia.working_memory import WorkingMemory from sia.working_memory import WorkingMemory
from sia.write_entry import WriteEntry from sia.write_entry import WriteEntry
class MockEntry(Entry): class MockEntry(Entry):
"""Mock entry class for testing""" """Mock entry class for testing"""
def __init__(self, id: str, timestamp: datetime): def __init__(self, id: str, timestamp: datetime):
@@ -32,6 +34,17 @@ class WorkingMemoryTest(unittest.TestCase):
self.memory = WorkingMemory() self.memory = WorkingMemory()
self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0) self.test_timestamp = datetime(2024, 10, 31, 12, 0, 0)
def wait_for_process_start(self, entry: BackgroundEntry, timeout=1.0):
"""Wait for process to start, checking context pid"""
start_time = time.time()
while time.time() - start_time < timeout:
entry.update()
context = entry.generate_context()
if context.get("pid") is not None:
return True
time.sleep(0.1)
return False
def test_initialization(self): def test_initialization(self):
"""Test initial state of working memory""" """Test initial state of working memory"""
self.assertEqual(self.memory.get_entries_count(), 0) self.assertEqual(self.memory.get_entries_count(), 0)
@@ -147,46 +160,40 @@ class WorkingMemoryTest(unittest.TestCase):
def test_remove_entry_cleanup(self): def test_remove_entry_cleanup(self):
"""Test that removing an entry triggers cleanup""" """Test that removing an entry triggers cleanup"""
# Create a background process entry # Create and start background process
entry = BackgroundEntry("sleep 10", "test-id", datetime(2024, 10, 31, 12, 0, 0)) entry = BackgroundEntry("sleep 10", "test-id", self.test_timestamp)
self.memory.add_entry(entry) self.memory.add_entry(entry)
# Start the process # Wait for process to start and get PID
entry.update() self.assertTrue(self.wait_for_process_start(entry))
self.assertIsNotNone(entry.process) context = entry.generate_context()
process_pid = entry.process.pid pid = int(context.get("pid"))
# Remove entry should trigger cleanup # Remove entry and verify process terminated
self.memory.remove_entry("test-id") self.memory.remove_entry(entry.id)
# Verify process was terminated
time.sleep(0.1) # Give process time to terminate time.sleep(0.1) # Give process time to terminate
with self.assertRaises(ProcessLookupError): with self.assertRaises(ProcessLookupError):
import os os.kill(pid, 0)
os.kill(process_pid, 0)
def test_cleanup_on_memory_clear(self): def test_cleanup_on_memory_clear(self):
"""Test that clearing memory properly cleans up all entries""" """Test that clearing memory properly cleans up all entries"""
# Add multiple background processes # Add multiple background processes
processes = [] pids = []
for i in range(3): for i in range(3):
entry = BackgroundEntry( entry = BackgroundEntry("sleep 10", f"id-{i}", self.test_timestamp)
"sleep 10",
f"id-{i}",
datetime(2024, 10, 31, 12, 0, 0)
)
self.memory.add_entry(entry) self.memory.add_entry(entry)
entry.update() self.assertTrue(self.wait_for_process_start(entry))
processes.append(entry.process.pid) context = entry.generate_context()
pids.append(int(context.get("pid")))
# Clear memory # Clear memory
self.memory.clear() self.memory.clear()
# Verify all processes were terminated # Verify all processes were terminated
time.sleep(0.1) # Give processes time to terminate time.sleep(0.1) # Give processes time to terminate
for pid in processes: for pid in pids:
with self.assertRaises(ProcessLookupError): with self.assertRaises(ProcessLookupError):
import os
os.kill(pid, 0) os.kill(pid, 0)
self.assertEqual(self.memory.get_entries_count(), 0) self.assertEqual(self.memory.get_entries_count(), 0)
@@ -194,14 +201,13 @@ class WorkingMemoryTest(unittest.TestCase):
def test_cleanup_on_memory_deletion(self): def test_cleanup_on_memory_deletion(self):
"""Test that deleting memory properly cleans up all entries""" """Test that deleting memory properly cleans up all entries"""
# Add a background process # Add a background process
entry = BackgroundEntry( entry = BackgroundEntry("sleep 10", "test-id", self.test_timestamp)
"sleep 10",
"test-id",
datetime(2024, 10, 31, 12, 0, 0)
)
self.memory.add_entry(entry) self.memory.add_entry(entry)
entry.update()
process_pid = entry.process.pid # Wait for process to start and get PID
self.assertTrue(self.wait_for_process_start(entry))
context = entry.generate_context()
pid = int(context.get("pid"))
# Delete memory # Delete memory
del self.memory del self.memory
@@ -209,8 +215,7 @@ class WorkingMemoryTest(unittest.TestCase):
# Verify process was terminated # Verify process was terminated
time.sleep(0.1) # Give process time to terminate time.sleep(0.1) # Give process time to terminate
with self.assertRaises(ProcessLookupError): with self.assertRaises(ProcessLookupError):
import os os.kill(pid, 0)
os.kill(process_pid, 0)
def test_get_entries(self): def test_get_entries(self):
"""Test getting all entries""" """Test getting all entries"""