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.
|
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)
|
||||||
|
|||||||
@ -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}")
|
||||||
|
|||||||
@ -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"],
|
||||||
|
|||||||
@ -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
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