added new testfiles and completed some testfiles with tests.

test_execution_engine, test_mcp_server_code_execution
test_mcp_server_files_search
test_mcp_server_web_search
müssen noch gemacht werden
This commit is contained in:
Livio Meuli 2026-05-21 10:46:01 +02:00
parent 9dedd0403c
commit 855c4edde4
11 changed files with 727 additions and 191 deletions

View File

@ -1,14 +1,66 @@
# Simple in-memory logger used during code execution. from datetime import datetime
# Messages are collected in a list and can be retrieved for display or debugging.
class DebugLogger: class DebugLogger:
"""In-memory logger for code execution events.
Collects timestamped INFO and ERROR entries during a single run.
Call clear() before each new execution to start fresh.
"""
def __init__(self): def __init__(self):
self.logs: list[dict] = []
def log(self, message: str) -> None:
"""Append a general info message."""
self.logs.append({
"level": "INFO",
"message": message,
"timestamp": datetime.now().strftime("%H:%M:%S"),
})
def log_error(self, error_message: str) -> None:
"""Append an error message."""
self.logs.append({
"level": "ERROR",
"message": error_message,
"timestamp": datetime.now().strftime("%H:%M:%S"),
})
def get_logs(self) -> list[dict]:
"""Return all collected log entries."""
return self.logs
def clear(self) -> None:
"""Reset the log — call before each new execution."""
self.logs = [] self.logs = []
def log(self, message): def format_debug_output(self, output: dict) -> str:
"""Append a log message to the in-memory list.""" """Format an ExecutionEngine result dict into a human-readable string.
self.logs.append(message)
def get_logs(self): Args:
"""Return all collected log messages.""" output: dict with keys 'stdout', 'stderr', and 'rc'.
return self.logs
Returns:
A formatted string ready for display in the UI.
"""
lines = []
status = "SUCCESS" if output.get("rc") == 0 else "FAILED"
lines.append(f"[{status}] Exit code: {output.get('rc')}")
if output.get("stdout"):
lines.append("\n--- stdout ---")
lines.append(output["stdout"].rstrip())
if output.get("stderr"):
lines.append("\n--- stderr ---")
lines.append(output["stderr"].rstrip())
if not output.get("stdout") and not output.get("stderr"):
lines.append("No output produced.")
for entry in self.logs:
lines.append(f"[{entry['timestamp']}] [{entry['level']}] {entry['message']}")
return "\n".join(lines)

View File

@ -238,7 +238,6 @@ class FileManager:
""" """
file_path = Path(relative_path) file_path = Path(relative_path)
abs_file_path = (Path(self.base_path) / file_path).resolve() abs_file_path = (Path(self.base_path) / file_path).resolve()
print(f"Absolute file path resolved to: {abs_file_path}") # Debugging info
if not str(abs_file_path).startswith(str(self.base_path.resolve())): if not str(abs_file_path).startswith(str(self.base_path.resolve())):
st.error(f"Access denied: {relative_path}") st.error(f"Access denied: {relative_path}")

View File

@ -98,12 +98,16 @@ def run_active_file():
execution_engine = ExecutionEngine() execution_engine = ExecutionEngine()
debug_logger = DebugLogger() debug_logger = DebugLogger()
debug_logger.clear()
debug_logger.log(f"Executing code from {active_file}...") debug_logger.log(f"Executing code from {active_file}...")
with st.spinner(f"Running {Path(active_file).name}..."): with st.spinner(f"Running {Path(active_file).name}..."):
output = execution_engine.run_code(Path(active_file)) output = execution_engine.run_code(Path(active_file))
debug_logger.log("Execution completed.") if output["rc"] == 0:
debug_logger.log("Execution completed successfully.")
else:
debug_logger.log_error(f"Execution failed with exit code {output['rc']}.")
st.session_state.code_execution_output = { st.session_state.code_execution_output = {
"stdout": output["stdout"], "stdout": output["stdout"],

View File

@ -1,242 +1,224 @@
"""Test script for ChatManager - Pytest compatible tests""" """Tests for ChatManager (backend/managers/chat_manager.py)."""
import sys import sys
from pathlib import Path from pathlib import Path
import pytest import pytest
import requests
from unittest.mock import patch, MagicMock from unittest.mock import patch, MagicMock
# Add project root to Python path
sys.path.insert(0, str(Path(__file__).parent.parent)) sys.path.insert(0, str(Path(__file__).parent.parent))
from backend.managers.chat_manager import ChatManager from backend.managers.chat_manager import ChatManager
class TestChatManager: # ── Helpers ──────────────────────────────────────────────────────────────────
"""Test suite for ChatManager functionality."""
def _mock_ok(content="AI reply"):
"""Return a mocked 200 response with a single assistant choice."""
mock = MagicMock()
mock.status_code = 200
mock.json.return_value = {
"choices": [{"message": {"role": "assistant", "content": content}}]
}
mock.text = ""
return mock
# ── Initialization ────────────────────────────────────────────────────────────
class TestChatManagerInit:
def test_chat_history_starts_empty(self):
cm = ChatManager()
assert cm.chat_history == []
def test_api_url_contains_endpoint(self):
cm = ChatManager()
assert isinstance(cm.api_url, str)
assert "/v1/chat/completions" in cm.api_url
# ── History management ────────────────────────────────────────────────────────
class TestHistory:
@pytest.fixture @pytest.fixture
def chat_manager(self): def cm(self):
return ChatManager() return ChatManager()
def test_initialization(self, chat_manager): def test_add_message_appends_correct_entry(self, cm):
"""Test that ChatManager initializes correctly.""" cm.add_message("user", "Hello")
assert chat_manager.api_url is not None assert cm.chat_history == [{"role": "user", "content": "Hello"}]
assert chat_manager.model is not None
assert chat_manager.chat_history == []
def test_add_message(self, chat_manager): def test_add_multiple_messages_preserves_order(self, cm):
"""Test adding messages to chat history.""" cm.add_message("user", "Hi")
chat_manager.add_message("user", "Hello") cm.add_message("assistant", "Hello!")
assert len(chat_manager.chat_history) == 1 assert cm.chat_history[0]["role"] == "user"
assert chat_manager.chat_history[0]["role"] == "user" assert cm.chat_history[1]["role"] == "assistant"
assert chat_manager.chat_history[0]["content"] == "Hello"
def test_get_history(self, chat_manager): def test_get_history_returns_internal_list(self, cm):
"""Test retrieving chat history.""" cm.add_message("user", "Hi")
chat_manager.add_message("user", "Hello") assert cm.get_history() is cm.chat_history
chat_manager.add_message("assistant", "Hi there!")
history = chat_manager.get_history() def test_clear_history_empties_list(self, cm):
assert len(history) == 2 cm.add_message("user", "Hi")
assert history[0]["role"] == "user" cm.clear_history()
assert history[1]["role"] == "assistant" assert cm.chat_history == []
def test_clear_history(self, chat_manager): def test_clear_history_on_empty_is_safe(self, cm):
"""Test clearing chat history.""" cm.clear_history()
chat_manager.add_message("user", "Hello") assert cm.chat_history == []
assert len(chat_manager.chat_history) == 1
chat_manager.clear_history()
assert len(chat_manager.chat_history) == 0
def test_send_message_integration(self, chat_manager):
"""
Integration test for sending message to AI.
This test actually communicates with the API.
"""
try:
# Send a simple test message
response = chat_manager.send_message("Hello, what is 2+2?")
# Verify response is not empty
assert isinstance(response, str)
assert len(response) > 0
# Verify message was added to history
assert len(chat_manager.chat_history) == 2 # user + assistant
assert chat_manager.chat_history[0]["role"] == "user"
assert chat_manager.chat_history[1]["role"] == "assistant"
print(f"API Test Passed")
print(f"Response: {response}")
except Exception as e:
# If API is not reachable, mark as skipped
pytest.skip(f"API not reachable: {str(e)}")
def test_multiple_messages(self, chat_manager):
"""Test sending multiple messages in a conversation."""
try:
# Send first message
response1 = chat_manager.send_message("What is your name?")
assert len(response1) > 0
# Send follow-up message
response2 = chat_manager.send_message("Tell me more")
assert len(response2) > 0
# Verify full conversation is in history
assert len(chat_manager.chat_history) == 4 # 2 user + 2 assistant
print(f"Conversation Test Passed")
print(f"Messages: {len(chat_manager.chat_history)}")
except Exception as e:
pytest.skip(f"API not reachable: {str(e)}")
class TestChatManagerSendMessage: # ── send_message (mocked HTTP) ────────────────────────────────────────────────
"""Unit tests for send_message using mocked HTTP requests."""
class TestSendMessage:
@pytest.fixture @pytest.fixture
def chat_manager(self): def cm(self):
return ChatManager() return ChatManager()
def _mock_response(self, content="AI reply", status_code=200): def test_user_message_added_to_history(self, cm):
mock = MagicMock() with patch("requests.post", return_value=_mock_ok()):
mock.status_code = status_code cm.send_message("Hello")
mock.json.return_value = { assert cm.chat_history[0] == {"role": "user", "content": "Hello"}
"choices": [{"message": {"role": "assistant", "content": content}}]
}
mock.text = "error text"
return mock
def test_send_message_adds_user_message_to_history(self, chat_manager): def test_assistant_reply_added_to_history(self, cm):
with patch("requests.post", return_value=self._mock_response()): with patch("requests.post", return_value=_mock_ok("Hi there")):
chat_manager.send_message("Hello") cm.send_message("Hello")
assert chat_manager.chat_history[0] == {"role": "user", "content": "Hello"} assert cm.chat_history[1] == {"role": "assistant", "content": "Hi there"}
def test_send_message_adds_assistant_response_to_history(self, chat_manager): def test_returns_assistant_content_string(self, cm):
with patch("requests.post", return_value=self._mock_response("Hi there")): with patch("requests.post", return_value=_mock_ok("Answer")):
chat_manager.send_message("Hello") result = cm.send_message("Question")
assert chat_manager.chat_history[1] == {"role": "assistant", "content": "Hi there"} assert result == "Answer"
def test_send_message_returns_ai_content(self, chat_manager): def test_full_history_sent_in_request_payload(self, cm):
with patch("requests.post", return_value=self._mock_response("Answer")): """All prior messages must be forwarded so the model has conversation context."""
response = chat_manager.send_message("Question") cm.add_message("system", "You are helpful.")
assert response == "Answer" with patch("requests.post", return_value=_mock_ok()) as mock_post:
cm.send_message("Hello")
payload = mock_post.call_args.kwargs["json"]
assert payload["messages"][0]["role"] == "system"
assert payload["messages"][1]["role"] == "user"
def test_send_message_history_grows_with_each_call(self, chat_manager): def test_history_grows_by_two_per_call(self, cm):
with patch("requests.post", return_value=self._mock_response()): with patch("requests.post", return_value=_mock_ok()):
chat_manager.send_message("First") cm.send_message("First")
chat_manager.send_message("Second") cm.send_message("Second")
assert len(chat_manager.chat_history) == 4 # 2 user + 2 assistant assert len(cm.chat_history) == 4
def test_send_message_connection_error_raises(self, chat_manager): def test_connection_error_raises_and_adds_error_to_history(self, cm):
import requests
with patch("requests.post", side_effect=requests.exceptions.ConnectionError("refused")): with patch("requests.post", side_effect=requests.exceptions.ConnectionError("refused")):
with pytest.raises(Exception, match="Connection Error"): with pytest.raises(Exception, match="Connection Error"):
chat_manager.send_message("Hello") cm.send_message("Hello")
assert any("Error" in msg["content"] for msg in cm.chat_history)
def test_send_message_api_error_status_raises(self, chat_manager): def test_api_error_status_raises(self, cm):
mock = self._mock_response(status_code=500) mock = MagicMock()
mock.status_code = 500
mock.text = "Internal Server Error"
with patch("requests.post", return_value=mock): with patch("requests.post", return_value=mock):
with pytest.raises(Exception, match="API Error 500"): with pytest.raises(Exception, match="API Error 500"):
chat_manager.send_message("Hello") cm.send_message("Hello")
def test_send_message_empty_choices_raises(self, chat_manager): def test_timeout_raises(self, cm):
with patch("requests.post", side_effect=requests.exceptions.Timeout()):
with pytest.raises(Exception):
cm.send_message("Hello")
def test_empty_choices_raises(self, cm):
mock = MagicMock() mock = MagicMock()
mock.status_code = 200 mock.status_code = 200
mock.json.return_value = {"choices": []} mock.json.return_value = {"choices": []}
with patch("requests.post", return_value=mock): with patch("requests.post", return_value=mock):
with pytest.raises(Exception, match="Invalid API response format"): with pytest.raises(Exception, match="Invalid API response format"):
chat_manager.send_message("Hello") cm.send_message("Hello")
def test_send_message_missing_choices_key_raises(self, chat_manager): def test_missing_choices_key_raises(self, cm):
mock = MagicMock() mock = MagicMock()
mock.status_code = 200 mock.status_code = 200
mock.json.return_value = {} mock.json.return_value = {}
with patch("requests.post", return_value=mock): with patch("requests.post", return_value=mock):
with pytest.raises(Exception): with pytest.raises(Exception):
chat_manager.send_message("Hello") cm.send_message("Hello")
def test_send_message_timeout_raises(self, chat_manager): def test_api_key_included_in_header_when_set(self, cm):
import requests cm.api_key = "test-key-123"
with patch("requests.post", side_effect=requests.exceptions.Timeout()): with patch("requests.post", return_value=_mock_ok()) as mock_post:
with pytest.raises(Exception): cm.send_message("Hello")
chat_manager.send_message("Hello") headers = mock_post.call_args.kwargs["headers"]
assert headers.get("Authorization") == "Bearer test-key-123"
def test_api_key_excluded_from_header_when_empty(self, cm):
cm.api_key = "EMPTY"
with patch("requests.post", return_value=_mock_ok()) as mock_post:
cm.send_message("Hello")
headers = mock_post.call_args.kwargs["headers"]
assert "Authorization" not in headers
def test_json_decode_error_raises(self, cm):
import json
mock = MagicMock()
mock.status_code = 200
mock.json.side_effect = json.JSONDecodeError("bad json", "", 0)
with patch("requests.post", return_value=mock):
with pytest.raises(Exception, match="JSON Decode Error"):
cm.send_message("Hello")
class TestChatManagerGetChatDisplay: # ── get_chat_display ──────────────────────────────────────────────────────────
"""Tests for get_chat_display()."""
class TestGetChatDisplay:
@pytest.fixture @pytest.fixture
def chat_manager(self): def cm(self):
return ChatManager() return ChatManager()
def test_empty_history_returns_empty_list(self, chat_manager): def test_empty_history_returns_empty_list(self, cm):
assert chat_manager.get_chat_display() == [] assert cm.get_chat_display() == []
def test_display_contains_role_and_content_keys(self, chat_manager): def test_display_has_role_and_content_keys(self, cm):
chat_manager.add_message("user", "Hello") cm.add_message("user", "Hello")
display = chat_manager.get_chat_display() entry = cm.get_chat_display()[0]
assert "role" in display[0] assert "role" in entry
assert "content" in display[0] assert "content" in entry
def test_display_preserves_message_order(self, chat_manager): def test_display_preserves_message_order(self, cm):
chat_manager.add_message("user", "First") cm.add_message("user", "First")
chat_manager.add_message("assistant", "Second") cm.add_message("assistant", "Second")
display = chat_manager.get_chat_display() display = cm.get_chat_display()
assert display[0]["role"] == "user" assert display[0]["role"] == "user"
assert display[1]["role"] == "assistant" assert display[1]["role"] == "assistant"
def test_display_matches_history(self, chat_manager): def test_display_returns_copy_not_reference(self, cm):
chat_manager.add_message("user", "Hi") """Mutating the returned list must not corrupt internal history."""
chat_manager.add_message("assistant", "Hello!") cm.add_message("user", "Hi")
assert chat_manager.get_chat_display() == chat_manager.get_history() display = cm.get_chat_display()
display.clear()
assert len(cm.chat_history) == 1
def test_system_message_included_in_display(self, chat_manager): def test_system_messages_included_in_display(self, cm):
chat_manager.add_message("system", "You are a helper.") cm.add_message("system", "Be helpful.")
display = chat_manager.get_chat_display() assert cm.get_chat_display()[0]["role"] == "system"
assert display[0]["role"] == "system"
def test_chat_manager_demo(): # ── Integration (skipped when API unreachable) ────────────────────────────────
"""Demo test - Shows interactive chat (can be run manually)."""
print("\n" + "=" * 60)
print("ChatManager Demo - Interactive Test")
print("=" * 60 + "\n")
chat_manager = ChatManager() class TestSendMessageIntegration:
@pytest.fixture
print(f"Connected to API: {chat_manager.api_url}") def cm(self):
print(f"Model: {chat_manager.model}\n") return ChatManager()
# Demo conversation
test_messages = ["Hello! What can you do?", "Tell me a joke", "What is Python?"]
print("Starting conversation...\n")
for message in test_messages:
print(f"User: {message}")
def test_real_api_returns_non_empty_string(self, cm):
try: try:
response = chat_manager.send_message(message) response = cm.send_message("Reply with exactly the word PONG.")
print(f"Assistant: {response}\n") assert isinstance(response, str)
assert len(response) > 0
assert len(cm.chat_history) == 2
except Exception as e: except Exception as e:
print(f"Error: {str(e)}\n") pytest.skip(f"API not reachable: {e}")
pytest.skip(f"API not reachable: {str(e)}")
# Display full chat history def test_real_api_multi_turn_history_grows(self, cm):
print("=" * 60) try:
print("Chat History:") cm.send_message("Remember the number 42.")
print("=" * 60) cm.send_message("What number did I ask you to remember?")
assert len(cm.chat_history) == 4
for msg in chat_manager.get_history(): except Exception as e:
print(f"{msg['role'].upper()}: {msg['content']}\n") pytest.skip(f"API not reachable: {e}")
if __name__ == "__main__":
# Run with: pytest tests/test_chat_manager.py -v -s
pytest.main([__file__, "-v", "-s"])

147
tests/test_debug_logger.py Normal file
View File

@ -0,0 +1,147 @@
"""Tests for DebugLogger (backend/managers/debug_logger.py)."""
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).parent.parent))
from backend.managers.debug_logger import DebugLogger
# ── Fixtures ──────────────────────────────────────────────────────────────────
@pytest.fixture
def logger():
return DebugLogger()
# ── Initialization ────────────────────────────────────────────────────────────
class TestInit:
def test_logs_start_empty(self, logger):
assert logger.logs == []
# ── log() ─────────────────────────────────────────────────────────────────────
class TestLog:
def test_log_appends_entry(self, logger):
logger.log("started")
assert len(logger.logs) == 1
def test_log_sets_level_info(self, logger):
logger.log("started")
assert logger.logs[0]["level"] == "INFO"
def test_log_stores_message(self, logger):
logger.log("executing file.py")
assert logger.logs[0]["message"] == "executing file.py"
def test_log_adds_timestamp(self, logger):
logger.log("started")
ts = logger.logs[0]["timestamp"]
# HH:MM:SS format — exactly 8 chars with two colons
assert len(ts) == 8
assert ts[2] == ":" and ts[5] == ":"
# ── log_error() ───────────────────────────────────────────────────────────────
class TestLogError:
def test_log_error_appends_entry(self, logger):
logger.log_error("something broke")
assert len(logger.logs) == 1
def test_log_error_sets_level_error(self, logger):
logger.log_error("something broke")
assert logger.logs[0]["level"] == "ERROR"
def test_log_error_stores_message(self, logger):
logger.log_error("exit code 1")
assert logger.logs[0]["message"] == "exit code 1"
def test_log_and_log_error_are_distinct_levels(self, logger):
logger.log("info message")
logger.log_error("error message")
assert logger.logs[0]["level"] == "INFO"
assert logger.logs[1]["level"] == "ERROR"
# ── get_logs() ────────────────────────────────────────────────────────────────
class TestGetLogs:
def test_get_logs_returns_all_entries(self, logger):
logger.log("first")
logger.log_error("second")
assert len(logger.get_logs()) == 2
def test_get_logs_returns_internal_list(self, logger):
assert logger.get_logs() is logger.logs
# ── clear() ───────────────────────────────────────────────────────────────────
class TestClear:
def test_clear_removes_all_entries(self, logger):
logger.log("first")
logger.log_error("second")
logger.clear()
assert logger.logs == []
def test_clear_on_empty_is_safe(self, logger):
logger.clear()
assert logger.logs == []
def test_log_after_clear_works(self, logger):
logger.log("before")
logger.clear()
logger.log("after")
assert len(logger.logs) == 1
assert logger.logs[0]["message"] == "after"
# ── format_debug_output() ─────────────────────────────────────────────────────
class TestFormatDebugOutput:
def test_success_exit_code_shows_success(self, logger):
result = logger.format_debug_output({"rc": 0, "stdout": "", "stderr": ""})
assert "[SUCCESS]" in result
def test_nonzero_exit_code_shows_failed(self, logger):
result = logger.format_debug_output({"rc": 1, "stdout": "", "stderr": ""})
assert "[FAILED]" in result
def test_exit_code_included_in_output(self, logger):
result = logger.format_debug_output({"rc": 42, "stdout": "", "stderr": ""})
assert "42" in result
def test_stdout_included_when_present(self, logger):
result = logger.format_debug_output({"rc": 0, "stdout": "Hello", "stderr": ""})
assert "Hello" in result
assert "stdout" in result
def test_stderr_included_when_present(self, logger):
result = logger.format_debug_output({"rc": 1, "stdout": "", "stderr": "NameError"})
assert "NameError" in result
assert "stderr" in result
def test_no_output_message_when_both_empty(self, logger):
result = logger.format_debug_output({"rc": 0, "stdout": "", "stderr": ""})
assert "No output produced." in result
def test_log_entries_appended_to_output(self, logger):
logger.log("Executing file.py...")
logger.log_error("exit code 1")
result = logger.format_debug_output({"rc": 1, "stdout": "", "stderr": ""})
assert "Executing file.py..." in result
assert "exit code 1" in result
def test_returns_string(self, logger):
result = logger.format_debug_output({"rc": 0, "stdout": "", "stderr": ""})
assert isinstance(result, str)
def test_missing_keys_do_not_raise(self, logger):
# Defensive: format_debug_output uses .get() so missing keys are safe
result = logger.format_debug_output({})
assert isinstance(result, str)

View File

@ -0,0 +1,216 @@
"""Tests for FileManager (backend/managers/file_manager.py)."""
import sys
from pathlib import Path
import pytest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).parent.parent))
from backend.managers.file_manager import FileManager
# ── Fixtures ──────────────────────────────────────────────────────────────────
@pytest.fixture(autouse=True)
def mock_streamlit():
"""Suppress all st.error / st.warning calls — they require a running Streamlit app."""
with patch("backend.managers.file_manager.st"):
yield
@pytest.fixture
def fm(tmp_path):
return FileManager(base_path=tmp_path)
# ── create_folder ─────────────────────────────────────────────────────────────
class TestCreateFolder:
def test_creates_folder_successfully(self, fm, tmp_path):
result = fm.create_folder("", "myfolder")
assert result is True
assert (tmp_path / "myfolder").is_dir()
def test_empty_name_returns_false(self, fm):
assert fm.create_folder("", "") is False
def test_slash_in_name_returns_false(self, fm):
assert fm.create_folder("", "a/b") is False
def test_backslash_in_name_returns_false(self, fm):
assert fm.create_folder("", "a\\b") is False
def test_duplicate_folder_returns_false(self, fm, tmp_path):
(tmp_path / "existing").mkdir()
assert fm.create_folder("", "existing") is False
def test_path_traversal_returns_false(self, fm):
assert fm.create_folder("../../", "evil") is False
def test_nested_folder_created_inside_base(self, fm, tmp_path):
(tmp_path / "sub").mkdir()
result = fm.create_folder("sub", "child")
assert result is True
assert (tmp_path / "sub" / "child").is_dir()
# ── create_file ───────────────────────────────────────────────────────────────
class TestCreateFile:
def test_creates_file_successfully(self, fm, tmp_path):
result = fm.create_file("", "test.py")
assert result is True
assert (tmp_path / "test.py").is_file()
def test_empty_name_returns_false(self, fm):
assert fm.create_file("", "") is False
def test_whitespace_only_name_returns_false(self, fm):
assert fm.create_file("", " ") is False
def test_no_extension_defaults_to_txt(self, fm, tmp_path):
fm.create_file("", "notes")
assert (tmp_path / "notes.txt").is_file()
def test_duplicate_file_returns_false(self, fm, tmp_path):
(tmp_path / "existing.py").touch()
assert fm.create_file("", "existing.py") is False
def test_path_traversal_returns_false(self, fm):
assert fm.create_file("../../", "evil.py") is False
# ── read_file ─────────────────────────────────────────────────────────────────
class TestReadFile:
def test_reads_file_content(self, fm, tmp_path):
f = tmp_path / "hello.py"
f.write_text("print('hello')")
assert fm.read_file(f) == "print('hello')"
def test_nonexistent_file_returns_empty_string(self, fm, tmp_path):
assert fm.read_file(tmp_path / "ghost.py") == ""
def test_directory_path_returns_empty_string(self, fm, tmp_path):
sub = tmp_path / "subdir"
sub.mkdir()
assert fm.read_file(sub) == ""
def test_file_outside_workspace_returns_empty_string(self, fm, tmp_path):
outside = tmp_path.parent / "outside.py"
outside.write_text("secret")
result = fm.read_file(outside)
assert result == ""
outside.unlink()
# ── save_file ─────────────────────────────────────────────────────────────────
class TestSaveFile:
def test_saves_content_to_file(self, fm, tmp_path):
f = tmp_path / "output.py"
f.touch()
result = fm.save_file(str(f), "x = 1")
assert result is True
assert f.read_text() == "x = 1"
def test_overwrites_existing_content(self, fm, tmp_path):
f = tmp_path / "script.py"
f.write_text("old content")
fm.save_file(str(f), "new content")
assert f.read_text() == "new content"
def test_path_traversal_returns_false(self, fm, tmp_path):
outside = str(tmp_path.parent / "evil.py")
assert fm.save_file(outside, "bad") is False
# ── rename_file ───────────────────────────────────────────────────────────────
class TestRenameFile:
def test_renames_file_successfully(self, fm, tmp_path):
(tmp_path / "old.py").touch()
result = fm.rename_file("old.py", "new")
assert result is True
assert (tmp_path / "new.py").exists()
assert not (tmp_path / "old.py").exists()
def test_preserves_original_extension(self, fm, tmp_path):
(tmp_path / "script.py").touch()
fm.rename_file("script.py", "renamed.txt")
assert (tmp_path / "renamed.py").exists()
def test_empty_new_name_returns_false(self, fm, tmp_path):
(tmp_path / "file.py").touch()
assert fm.rename_file("file.py", "") is False
def test_nonexistent_file_returns_false(self, fm):
assert fm.rename_file("ghost.py", "new_name") is False
def test_path_traversal_returns_false(self, fm):
assert fm.rename_file("../../evil.py", "new_name") is False
# ── delete_file ───────────────────────────────────────────────────────────────
class TestDeleteFile:
def test_deletes_file_successfully(self, fm, tmp_path):
f = tmp_path / "todelete.py"
f.touch()
result = fm.delete_file("todelete.py")
assert result is True
assert not f.exists()
def test_nonexistent_file_returns_false(self, fm):
assert fm.delete_file("ghost.py") is False
def test_path_traversal_returns_false(self, fm):
assert fm.delete_file("../../evil.py") is False
# ── delete_folder ─────────────────────────────────────────────────────────────
class TestDeleteFolder:
def test_deletes_folder_and_contents(self, fm, tmp_path):
sub = tmp_path / "todelete"
sub.mkdir()
(sub / "file.py").touch()
result = fm.delete_folder("todelete")
assert result is True
assert not sub.exists()
def test_nonexistent_folder_returns_false(self, fm):
assert fm.delete_folder("ghost") is False
def test_path_traversal_returns_false(self, fm):
assert fm.delete_folder("../../") is False
# ── get_file_tree ─────────────────────────────────────────────────────────────
class TestGetFileTree:
def test_empty_workspace_returns_empty_dict(self, fm):
assert fm.get_file_tree() == {}
def test_file_is_represented_as_none(self, fm, tmp_path):
(tmp_path / "main.py").touch()
tree = fm.get_file_tree()
assert tree["main.py"] is None
def test_directory_is_represented_as_dict(self, fm, tmp_path):
(tmp_path / "src").mkdir()
tree = fm.get_file_tree()
assert isinstance(tree["src"], dict)
def test_nested_structure_is_correct(self, fm, tmp_path):
(tmp_path / "src").mkdir()
(tmp_path / "src" / "app.py").touch()
tree = fm.get_file_tree()
assert tree["src"]["app.py"] is None
def test_entries_are_sorted(self, fm, tmp_path):
(tmp_path / "z_file.py").touch()
(tmp_path / "a_file.py").touch()
keys = list(fm.get_file_tree().keys())
assert keys == sorted(keys)

View File

@ -1,6 +0,0 @@
"""Test cases for the main module."""
def test_placeholder():
"""Placeholder test."""
assert True

View File

View File

View File

142
tests/test_state.py Normal file
View File

@ -0,0 +1,142 @@
"""Tests for frontend/state.py — init_state() session initialisation."""
import sys
from pathlib import Path
import pytest
from unittest.mock import patch
sys.path.insert(0, str(Path(__file__).parent.parent))
from backend.managers.chat_manager import ChatManager
# ── Fake session_state ────────────────────────────────────────────────────────
class FakeSessionState:
"""Minimal stand-in for st.session_state that supports attribute and 'in' access."""
def __init__(self):
self._data = {}
def __contains__(self, key):
return key in self._data
def __setattr__(self, name, value):
if name == "_data":
super().__setattr__(name, value)
else:
self._data[name] = value
def __getattr__(self, name):
try:
return self._data[name]
except KeyError:
raise AttributeError(name)
def get(self, key, default=None):
return self._data.get(key, default)
@pytest.fixture
def fake_state():
state = FakeSessionState()
with patch("frontend.state.st.session_state", state):
yield state
# ── Key initialisation ────────────────────────────────────────────────────────
class TestInitStateKeys:
EXPECTED_KEYS = [
"last_selected",
"selected_folder",
"selected_folder_rel",
"chat_manager",
"open_files",
"files_content",
"active_file",
"active_tab",
"is_editing",
"code_suggestions",
"code_execution_output",
"chat_history",
"agent_mode",
"coding_agent",
"agent_status",
"agent_log",
"agent_pending_action",
]
def test_all_expected_keys_are_set(self, fake_state):
from frontend.state import init_state
init_state()
for key in self.EXPECTED_KEYS:
assert key in fake_state, f"Missing key: {key}"
def test_open_files_initialised_as_empty_list(self, fake_state):
from frontend.state import init_state
init_state()
assert fake_state.open_files == []
def test_files_content_initialised_as_empty_dict(self, fake_state):
from frontend.state import init_state
init_state()
assert fake_state.files_content == {}
def test_active_file_initialised_as_none(self, fake_state):
from frontend.state import init_state
init_state()
assert fake_state.active_file is None
def test_chat_history_initialised_as_empty_list(self, fake_state):
from frontend.state import init_state
init_state()
assert fake_state.chat_history == []
def test_agent_mode_initialised_as_false(self, fake_state):
from frontend.state import init_state
init_state()
assert fake_state.agent_mode is False
def test_agent_status_initialised_as_idle(self, fake_state):
from frontend.state import init_state
init_state()
assert fake_state.agent_status == "idle"
def test_chat_manager_is_chat_manager_instance(self, fake_state):
from frontend.state import init_state
init_state()
assert isinstance(fake_state.chat_manager, ChatManager)
# ── Idempotency ───────────────────────────────────────────────────────────────
class TestInitStateIdempotency:
def test_second_call_does_not_overwrite_open_files(self, fake_state):
"""init_state() must not reset state that was set by the user."""
from frontend.state import init_state
init_state()
fake_state.open_files = ["/workspace/file.py"]
init_state()
assert fake_state.open_files == ["/workspace/file.py"]
def test_second_call_does_not_overwrite_chat_history(self, fake_state):
from frontend.state import init_state
init_state()
fake_state.chat_history = [{"role": "user", "content": "Hi"}]
init_state()
assert len(fake_state.chat_history) == 1
def test_second_call_does_not_replace_chat_manager(self, fake_state):
from frontend.state import init_state
init_state()
original = fake_state.chat_manager
init_state()
assert fake_state.chat_manager is original
def test_second_call_does_not_overwrite_active_file(self, fake_state):
from frontend.state import init_state
init_state()
fake_state.active_file = "/workspace/main.py"
init_state()
assert fake_state.active_file == "/workspace/main.py"