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:
parent
9dedd0403c
commit
855c4edde4
@ -1,14 +1,66 @@
|
||||
# Simple in-memory logger used during code execution.
|
||||
# Messages are collected in a list and can be retrieved for display or debugging.
|
||||
from datetime import datetime
|
||||
|
||||
|
||||
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):
|
||||
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 = []
|
||||
|
||||
def log(self, message):
|
||||
"""Append a log message to the in-memory list."""
|
||||
self.logs.append(message)
|
||||
def format_debug_output(self, output: dict) -> str:
|
||||
"""Format an ExecutionEngine result dict into a human-readable string.
|
||||
|
||||
def get_logs(self):
|
||||
"""Return all collected log messages."""
|
||||
return self.logs
|
||||
Args:
|
||||
output: dict with keys 'stdout', 'stderr', and 'rc'.
|
||||
|
||||
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)
|
||||
|
||||
@ -238,7 +238,6 @@ class FileManager:
|
||||
"""
|
||||
file_path = Path(relative_path)
|
||||
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())):
|
||||
st.error(f"Access denied: {relative_path}")
|
||||
|
||||
@ -98,12 +98,16 @@ def run_active_file():
|
||||
execution_engine = ExecutionEngine()
|
||||
debug_logger = DebugLogger()
|
||||
|
||||
debug_logger.clear()
|
||||
debug_logger.log(f"Executing code from {active_file}...")
|
||||
|
||||
with st.spinner(f"Running {Path(active_file).name}..."):
|
||||
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 = {
|
||||
"stdout": output["stdout"],
|
||||
|
||||
@ -1,242 +1,224 @@
|
||||
"""Test script for ChatManager - Pytest compatible tests"""
|
||||
"""Tests for ChatManager (backend/managers/chat_manager.py)."""
|
||||
|
||||
import sys
|
||||
from pathlib import Path
|
||||
import pytest
|
||||
import requests
|
||||
from unittest.mock import patch, MagicMock
|
||||
|
||||
# Add project root to Python path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
from backend.managers.chat_manager import ChatManager
|
||||
|
||||
|
||||
class TestChatManager:
|
||||
"""Test suite for ChatManager functionality."""
|
||||
# ── Helpers ──────────────────────────────────────────────────────────────────
|
||||
|
||||
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
|
||||
def chat_manager(self):
|
||||
def cm(self):
|
||||
return ChatManager()
|
||||
|
||||
def test_initialization(self, chat_manager):
|
||||
"""Test that ChatManager initializes correctly."""
|
||||
assert chat_manager.api_url is not None
|
||||
assert chat_manager.model is not None
|
||||
assert chat_manager.chat_history == []
|
||||
def test_add_message_appends_correct_entry(self, cm):
|
||||
cm.add_message("user", "Hello")
|
||||
assert cm.chat_history == [{"role": "user", "content": "Hello"}]
|
||||
|
||||
def test_add_message(self, chat_manager):
|
||||
"""Test adding messages to chat history."""
|
||||
chat_manager.add_message("user", "Hello")
|
||||
assert len(chat_manager.chat_history) == 1
|
||||
assert chat_manager.chat_history[0]["role"] == "user"
|
||||
assert chat_manager.chat_history[0]["content"] == "Hello"
|
||||
def test_add_multiple_messages_preserves_order(self, cm):
|
||||
cm.add_message("user", "Hi")
|
||||
cm.add_message("assistant", "Hello!")
|
||||
assert cm.chat_history[0]["role"] == "user"
|
||||
assert cm.chat_history[1]["role"] == "assistant"
|
||||
|
||||
def test_get_history(self, chat_manager):
|
||||
"""Test retrieving chat history."""
|
||||
chat_manager.add_message("user", "Hello")
|
||||
chat_manager.add_message("assistant", "Hi there!")
|
||||
def test_get_history_returns_internal_list(self, cm):
|
||||
cm.add_message("user", "Hi")
|
||||
assert cm.get_history() is cm.chat_history
|
||||
|
||||
history = chat_manager.get_history()
|
||||
assert len(history) == 2
|
||||
assert history[0]["role"] == "user"
|
||||
assert history[1]["role"] == "assistant"
|
||||
def test_clear_history_empties_list(self, cm):
|
||||
cm.add_message("user", "Hi")
|
||||
cm.clear_history()
|
||||
assert cm.chat_history == []
|
||||
|
||||
def test_clear_history(self, chat_manager):
|
||||
"""Test clearing chat history."""
|
||||
chat_manager.add_message("user", "Hello")
|
||||
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)}")
|
||||
def test_clear_history_on_empty_is_safe(self, cm):
|
||||
cm.clear_history()
|
||||
assert cm.chat_history == []
|
||||
|
||||
|
||||
class TestChatManagerSendMessage:
|
||||
"""Unit tests for send_message using mocked HTTP requests."""
|
||||
# ── send_message (mocked HTTP) ────────────────────────────────────────────────
|
||||
|
||||
class TestSendMessage:
|
||||
@pytest.fixture
|
||||
def chat_manager(self):
|
||||
def cm(self):
|
||||
return ChatManager()
|
||||
|
||||
def _mock_response(self, content="AI reply", status_code=200):
|
||||
mock = MagicMock()
|
||||
mock.status_code = status_code
|
||||
mock.json.return_value = {
|
||||
"choices": [{"message": {"role": "assistant", "content": content}}]
|
||||
}
|
||||
mock.text = "error text"
|
||||
return mock
|
||||
def test_user_message_added_to_history(self, cm):
|
||||
with patch("requests.post", return_value=_mock_ok()):
|
||||
cm.send_message("Hello")
|
||||
assert cm.chat_history[0] == {"role": "user", "content": "Hello"}
|
||||
|
||||
def test_send_message_adds_user_message_to_history(self, chat_manager):
|
||||
with patch("requests.post", return_value=self._mock_response()):
|
||||
chat_manager.send_message("Hello")
|
||||
assert chat_manager.chat_history[0] == {"role": "user", "content": "Hello"}
|
||||
def test_assistant_reply_added_to_history(self, cm):
|
||||
with patch("requests.post", return_value=_mock_ok("Hi there")):
|
||||
cm.send_message("Hello")
|
||||
assert cm.chat_history[1] == {"role": "assistant", "content": "Hi there"}
|
||||
|
||||
def test_send_message_adds_assistant_response_to_history(self, chat_manager):
|
||||
with patch("requests.post", return_value=self._mock_response("Hi there")):
|
||||
chat_manager.send_message("Hello")
|
||||
assert chat_manager.chat_history[1] == {"role": "assistant", "content": "Hi there"}
|
||||
def test_returns_assistant_content_string(self, cm):
|
||||
with patch("requests.post", return_value=_mock_ok("Answer")):
|
||||
result = cm.send_message("Question")
|
||||
assert result == "Answer"
|
||||
|
||||
def test_send_message_returns_ai_content(self, chat_manager):
|
||||
with patch("requests.post", return_value=self._mock_response("Answer")):
|
||||
response = chat_manager.send_message("Question")
|
||||
assert response == "Answer"
|
||||
def test_full_history_sent_in_request_payload(self, cm):
|
||||
"""All prior messages must be forwarded so the model has conversation context."""
|
||||
cm.add_message("system", "You are helpful.")
|
||||
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):
|
||||
with patch("requests.post", return_value=self._mock_response()):
|
||||
chat_manager.send_message("First")
|
||||
chat_manager.send_message("Second")
|
||||
assert len(chat_manager.chat_history) == 4 # 2 user + 2 assistant
|
||||
def test_history_grows_by_two_per_call(self, cm):
|
||||
with patch("requests.post", return_value=_mock_ok()):
|
||||
cm.send_message("First")
|
||||
cm.send_message("Second")
|
||||
assert len(cm.chat_history) == 4
|
||||
|
||||
def test_send_message_connection_error_raises(self, chat_manager):
|
||||
import requests
|
||||
def test_connection_error_raises_and_adds_error_to_history(self, cm):
|
||||
with patch("requests.post", side_effect=requests.exceptions.ConnectionError("refused")):
|
||||
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):
|
||||
mock = self._mock_response(status_code=500)
|
||||
def test_api_error_status_raises(self, cm):
|
||||
mock = MagicMock()
|
||||
mock.status_code = 500
|
||||
mock.text = "Internal Server Error"
|
||||
with patch("requests.post", return_value=mock):
|
||||
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.status_code = 200
|
||||
mock.json.return_value = {"choices": []}
|
||||
with patch("requests.post", return_value=mock):
|
||||
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.status_code = 200
|
||||
mock.json.return_value = {}
|
||||
with patch("requests.post", return_value=mock):
|
||||
with pytest.raises(Exception):
|
||||
chat_manager.send_message("Hello")
|
||||
cm.send_message("Hello")
|
||||
|
||||
def test_send_message_timeout_raises(self, chat_manager):
|
||||
import requests
|
||||
with patch("requests.post", side_effect=requests.exceptions.Timeout()):
|
||||
with pytest.raises(Exception):
|
||||
chat_manager.send_message("Hello")
|
||||
def test_api_key_included_in_header_when_set(self, cm):
|
||||
cm.api_key = "test-key-123"
|
||||
with patch("requests.post", return_value=_mock_ok()) as mock_post:
|
||||
cm.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:
|
||||
"""Tests for get_chat_display()."""
|
||||
# ── get_chat_display ──────────────────────────────────────────────────────────
|
||||
|
||||
class TestGetChatDisplay:
|
||||
@pytest.fixture
|
||||
def chat_manager(self):
|
||||
def cm(self):
|
||||
return ChatManager()
|
||||
|
||||
def test_empty_history_returns_empty_list(self, chat_manager):
|
||||
assert chat_manager.get_chat_display() == []
|
||||
def test_empty_history_returns_empty_list(self, cm):
|
||||
assert cm.get_chat_display() == []
|
||||
|
||||
def test_display_contains_role_and_content_keys(self, chat_manager):
|
||||
chat_manager.add_message("user", "Hello")
|
||||
display = chat_manager.get_chat_display()
|
||||
assert "role" in display[0]
|
||||
assert "content" in display[0]
|
||||
def test_display_has_role_and_content_keys(self, cm):
|
||||
cm.add_message("user", "Hello")
|
||||
entry = cm.get_chat_display()[0]
|
||||
assert "role" in entry
|
||||
assert "content" in entry
|
||||
|
||||
def test_display_preserves_message_order(self, chat_manager):
|
||||
chat_manager.add_message("user", "First")
|
||||
chat_manager.add_message("assistant", "Second")
|
||||
display = chat_manager.get_chat_display()
|
||||
def test_display_preserves_message_order(self, cm):
|
||||
cm.add_message("user", "First")
|
||||
cm.add_message("assistant", "Second")
|
||||
display = cm.get_chat_display()
|
||||
assert display[0]["role"] == "user"
|
||||
assert display[1]["role"] == "assistant"
|
||||
|
||||
def test_display_matches_history(self, chat_manager):
|
||||
chat_manager.add_message("user", "Hi")
|
||||
chat_manager.add_message("assistant", "Hello!")
|
||||
assert chat_manager.get_chat_display() == chat_manager.get_history()
|
||||
def test_display_returns_copy_not_reference(self, cm):
|
||||
"""Mutating the returned list must not corrupt internal history."""
|
||||
cm.add_message("user", "Hi")
|
||||
display = cm.get_chat_display()
|
||||
display.clear()
|
||||
assert len(cm.chat_history) == 1
|
||||
|
||||
def test_system_message_included_in_display(self, chat_manager):
|
||||
chat_manager.add_message("system", "You are a helper.")
|
||||
display = chat_manager.get_chat_display()
|
||||
assert display[0]["role"] == "system"
|
||||
def test_system_messages_included_in_display(self, cm):
|
||||
cm.add_message("system", "Be helpful.")
|
||||
assert cm.get_chat_display()[0]["role"] == "system"
|
||||
|
||||
|
||||
def test_chat_manager_demo():
|
||||
"""Demo test - Shows interactive chat (can be run manually)."""
|
||||
print("\n" + "=" * 60)
|
||||
print("ChatManager Demo - Interactive Test")
|
||||
print("=" * 60 + "\n")
|
||||
# ── Integration (skipped when API unreachable) ────────────────────────────────
|
||||
|
||||
chat_manager = ChatManager()
|
||||
|
||||
print(f"Connected to API: {chat_manager.api_url}")
|
||||
print(f"Model: {chat_manager.model}\n")
|
||||
|
||||
# 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}")
|
||||
class TestSendMessageIntegration:
|
||||
@pytest.fixture
|
||||
def cm(self):
|
||||
return ChatManager()
|
||||
|
||||
def test_real_api_returns_non_empty_string(self, cm):
|
||||
try:
|
||||
response = chat_manager.send_message(message)
|
||||
print(f"Assistant: {response}\n")
|
||||
|
||||
response = cm.send_message("Reply with exactly the word PONG.")
|
||||
assert isinstance(response, str)
|
||||
assert len(response) > 0
|
||||
assert len(cm.chat_history) == 2
|
||||
except Exception as e:
|
||||
print(f"Error: {str(e)}\n")
|
||||
pytest.skip(f"API not reachable: {str(e)}")
|
||||
pytest.skip(f"API not reachable: {e}")
|
||||
|
||||
# Display full chat history
|
||||
print("=" * 60)
|
||||
print("Chat History:")
|
||||
print("=" * 60)
|
||||
|
||||
for msg in chat_manager.get_history():
|
||||
print(f"{msg['role'].upper()}: {msg['content']}\n")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
# Run with: pytest tests/test_chat_manager.py -v -s
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
def test_real_api_multi_turn_history_grows(self, cm):
|
||||
try:
|
||||
cm.send_message("Remember the number 42.")
|
||||
cm.send_message("What number did I ask you to remember?")
|
||||
assert len(cm.chat_history) == 4
|
||||
except Exception as e:
|
||||
pytest.skip(f"API not reachable: {e}")
|
||||
|
||||
147
tests/test_debug_logger.py
Normal file
147
tests/test_debug_logger.py
Normal 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)
|
||||
@ -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)
|
||||
@ -1,6 +0,0 @@
|
||||
"""Test cases for the main module."""
|
||||
|
||||
|
||||
def test_placeholder():
|
||||
"""Placeholder test."""
|
||||
assert True
|
||||
0
tests/test_mcp_server_code_execution.py
Normal file
0
tests/test_mcp_server_code_execution.py
Normal file
0
tests/test_mcp_server_file_search.py
Normal file
0
tests/test_mcp_server_file_search.py
Normal file
0
tests/test_mcp_server_web_search.py
Normal file
0
tests/test_mcp_server_web_search.py
Normal file
142
tests/test_state.py
Normal file
142
tests/test_state.py
Normal 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"
|
||||
Loading…
x
Reference in New Issue
Block a user