remove some testcases
This commit is contained in:
parent
1c6e4a2b9d
commit
f91707b5b5
@ -24,25 +24,10 @@ def _mock_ok(content="AI reply"):
|
||||
return mock
|
||||
|
||||
|
||||
# ── Initialization ────────────────────────────────────────────────────────────
|
||||
|
||||
class TestChatManagerInit:
|
||||
"""Verify that a fresh ChatManager starts in a clean, predictable state."""
|
||||
|
||||
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:
|
||||
"""Tests for add_message, get_history, and clear_history."""
|
||||
"""Tests for add_message and clear_history."""
|
||||
|
||||
@pytest.fixture
|
||||
def cm(self):
|
||||
@ -58,22 +43,11 @@ class TestHistory:
|
||||
assert cm.chat_history[0]["role"] == "user"
|
||||
assert cm.chat_history[1]["role"] == "assistant"
|
||||
|
||||
def test_get_history_returns_copy_not_reference(self, cm):
|
||||
"""Mutating the returned list must not corrupt internal history."""
|
||||
cm.add_message("user", "Hi")
|
||||
history = cm.get_history()
|
||||
assert history == cm.chat_history
|
||||
assert history is not cm.chat_history
|
||||
|
||||
def test_clear_history_empties_list(self, cm):
|
||||
cm.add_message("user", "Hi")
|
||||
cm.clear_history()
|
||||
assert cm.chat_history == []
|
||||
|
||||
def test_clear_history_on_empty_is_safe(self, cm):
|
||||
cm.clear_history()
|
||||
assert cm.chat_history == []
|
||||
|
||||
|
||||
# ── send_message (mocked HTTP) ────────────────────────────────────────────────
|
||||
|
||||
@ -108,12 +82,6 @@ class TestSendMessage:
|
||||
assert payload["messages"][0]["role"] == "system"
|
||||
assert payload["messages"][1]["role"] == "user"
|
||||
|
||||
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_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"):
|
||||
@ -141,14 +109,6 @@ class TestSendMessage:
|
||||
with pytest.raises(Exception, match="Invalid API response format"):
|
||||
cm.send_message("Hello")
|
||||
|
||||
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):
|
||||
cm.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:
|
||||
@ -164,13 +124,6 @@ class TestSendMessage:
|
||||
headers = mock_post.call_args.kwargs["headers"]
|
||||
assert "Authorization" not in headers
|
||||
|
||||
def test_api_key_excluded_from_header_when_empty_string(self, cm):
|
||||
cm.api_key = ""
|
||||
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()
|
||||
@ -184,15 +137,12 @@ class TestSendMessage:
|
||||
# ── get_chat_display ──────────────────────────────────────────────────────────
|
||||
|
||||
class TestGetChatDisplay:
|
||||
"""Tests for get_chat_display: correct shape, ordering, isolation, and role coverage."""
|
||||
"""Tests for get_chat_display: correct shape and ordering."""
|
||||
|
||||
@pytest.fixture
|
||||
def cm(self):
|
||||
return ChatManager()
|
||||
|
||||
def test_empty_history_returns_empty_list(self, cm):
|
||||
assert cm.get_chat_display() == []
|
||||
|
||||
def test_display_has_role_and_content_keys(self, cm):
|
||||
cm.add_message("user", "Hello")
|
||||
entry = cm.get_chat_display()[0]
|
||||
@ -205,41 +155,3 @@ class TestGetChatDisplay:
|
||||
display = cm.get_chat_display()
|
||||
assert display[0]["role"] == "user"
|
||||
assert display[1]["role"] == "assistant"
|
||||
|
||||
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_messages_included_in_display(self, cm):
|
||||
cm.add_message("system", "Be helpful.")
|
||||
assert cm.get_chat_display()[0]["role"] == "system"
|
||||
|
||||
|
||||
# ── Integration (skipped when API unreachable) ────────────────────────────────
|
||||
|
||||
class TestSendMessageIntegration:
|
||||
"""End-to-end tests against the live API. Skipped automatically when the API is unreachable."""
|
||||
|
||||
@pytest.fixture
|
||||
def cm(self):
|
||||
return ChatManager()
|
||||
|
||||
def test_real_api_returns_non_empty_string(self, cm):
|
||||
try:
|
||||
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:
|
||||
pytest.skip(f"API not reachable: {e}")
|
||||
|
||||
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}")
|
||||
|
||||
@ -73,10 +73,6 @@ class TestTruncateResult:
|
||||
assert len(result) < len(long)
|
||||
assert "TRUNCATED" in result
|
||||
|
||||
def test_exact_limit_not_truncated(self):
|
||||
text = "a" * MAX_RESULT_LENGTH
|
||||
assert truncate_result(text) == text
|
||||
|
||||
def test_truncated_keeps_start_and_end(self):
|
||||
text = "START" + "x" * MAX_RESULT_LENGTH + "END"
|
||||
result = truncate_result(text)
|
||||
@ -144,9 +140,6 @@ class TestStripCodeFences:
|
||||
text = "```\nhello\n```"
|
||||
assert _strip_code_fences(text) == "hello"
|
||||
|
||||
def test_strips_whitespace(self):
|
||||
assert _strip_code_fences(" hello ") == "hello"
|
||||
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# TestCodingAgentInit
|
||||
@ -162,11 +155,6 @@ class TestCodingAgentInit:
|
||||
assert agent.is_done is False
|
||||
assert agent.iteration == 0
|
||||
|
||||
def test_api_url_is_set(self):
|
||||
agent = CodingAgent()
|
||||
assert agent.api_url.startswith("http://")
|
||||
assert "/v1/chat/completions" in agent.api_url
|
||||
|
||||
def test_start_task_sets_messages(self):
|
||||
agent = CodingAgent()
|
||||
agent.start_task("Write fibonacci.py")
|
||||
@ -397,55 +385,3 @@ class TestReject:
|
||||
assert not list(tmp_path.glob("*"))
|
||||
|
||||
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
# TestFullLoop (integration – real API, skipped if unreachable)
|
||||
# ═════════════════════════════════════════════════════════════════════════════
|
||||
|
||||
class TestFullLoop:
|
||||
"""End-to-end test: agent runs a real task against the live API.
|
||||
Skipped automatically if the API is not reachable.
|
||||
"""
|
||||
|
||||
MAX_STEPS = 15
|
||||
|
||||
async def _run_until_done(self, agent) -> list:
|
||||
steps = []
|
||||
for _ in range(self.MAX_STEPS):
|
||||
action = await agent.propose_next_action()
|
||||
result = await agent.approve()
|
||||
steps.append(result)
|
||||
if result["is_done"]:
|
||||
break
|
||||
return steps
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_completes_hello_world_task(self, tmp_path):
|
||||
try:
|
||||
with patch("backend.agent.coding_agent.WORKSPACE", tmp_path):
|
||||
agent = CodingAgent()
|
||||
agent.start_task(
|
||||
"Write a Python file called hello.py that prints 'Hello World'. "
|
||||
"Validate it and run it."
|
||||
)
|
||||
steps = await self._run_until_done(agent)
|
||||
assert agent.is_done, "Agent did not reach done state"
|
||||
tools_used = [s["tool"] for s in steps]
|
||||
assert "done" in tools_used
|
||||
except Exception as e:
|
||||
pytest.skip(f"API not reachable or environment incomplete: {e}")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_agent_creates_file_on_disk(self, tmp_path):
|
||||
try:
|
||||
with patch("backend.agent.coding_agent.WORKSPACE", tmp_path):
|
||||
agent = CodingAgent()
|
||||
agent.start_task("Write a file called output.txt containing the text 'test passed'.")
|
||||
await self._run_until_done(agent)
|
||||
py_files = list(tmp_path.glob("*.txt")) + list(tmp_path.glob("*.py"))
|
||||
assert len(py_files) > 0, "Agent did not create any file"
|
||||
except Exception as e:
|
||||
pytest.skip(f"API not reachable or environment incomplete: {e}")
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
pytest.main([__file__, "-v", "-s"])
|
||||
|
||||
@ -17,19 +17,10 @@ def logger():
|
||||
return DebugLogger()
|
||||
|
||||
|
||||
# ── Initialization ────────────────────────────────────────────────────────────
|
||||
|
||||
class TestInit:
|
||||
"""Tests that a fresh DebugLogger starts with an empty log list."""
|
||||
|
||||
def test_logs_start_empty(self, logger):
|
||||
assert logger.logs == []
|
||||
|
||||
|
||||
# ── log() ─────────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestLog:
|
||||
"""Tests for log(): each call appends an INFO-level entry with message and timestamp."""
|
||||
"""Tests for log(): each call appends an INFO-level entry with message."""
|
||||
|
||||
def test_log_appends_entry(self, logger):
|
||||
logger.log("started")
|
||||
@ -43,31 +34,16 @@ class TestLog:
|
||||
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:
|
||||
"""Tests for log_error(): same shape as log() but level is ERROR, not INFO."""
|
||||
|
||||
def test_log_error_appends_entry(self, logger):
|
||||
logger.log_error("something broke")
|
||||
assert len(logger.logs) == 1
|
||||
"""Tests for log_error(): level is ERROR, not INFO."""
|
||||
|
||||
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")
|
||||
@ -78,24 +54,18 @@ class TestLogError:
|
||||
# ── get_logs() ────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestGetLogs:
|
||||
"""Tests for get_logs(): returns all entries and a copy, not a reference to the internal list."""
|
||||
"""Tests for get_logs()."""
|
||||
|
||||
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_copy_not_reference(self, logger):
|
||||
logger.log("entry")
|
||||
logs = logger.get_logs()
|
||||
assert logs == logger.logs
|
||||
assert logs is not logger.logs
|
||||
|
||||
|
||||
# ── clear() ───────────────────────────────────────────────────────────────────
|
||||
|
||||
class TestClear:
|
||||
"""Tests for clear(): empties the log list and leaves the logger ready for reuse."""
|
||||
"""Tests for clear()."""
|
||||
|
||||
def test_clear_removes_all_entries(self, logger):
|
||||
logger.log("first")
|
||||
@ -103,23 +73,11 @@ class TestClear:
|
||||
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:
|
||||
"""Tests for format_debug_output(): renders a dict of {rc, stdout, stderr} plus
|
||||
accumulated log entries into a human-readable string for the UI."""
|
||||
"""Tests for format_debug_output(): renders rc, stdout, stderr, and log entries."""
|
||||
|
||||
def test_success_exit_code_shows_success(self, logger):
|
||||
result = logger.format_debug_output({"rc": 0, "stdout": "", "stderr": ""})
|
||||
@ -129,10 +87,6 @@ class TestFormatDebugOutput:
|
||||
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
|
||||
@ -143,10 +97,6 @@ class TestFormatDebugOutput:
|
||||
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")
|
||||
@ -154,10 +104,6 @@ class TestFormatDebugOutput:
|
||||
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({})
|
||||
|
||||
@ -99,11 +99,6 @@ class TestReadFile:
|
||||
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")
|
||||
@ -195,9 +190,6 @@ class TestDeleteFolder:
|
||||
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
|
||||
|
||||
@ -226,8 +218,3 @@ class TestGetFileTree:
|
||||
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)
|
||||
|
||||
@ -6,33 +6,17 @@ from pathlib import Path
|
||||
sys.path.insert(0, str(Path(__file__).parent.parent))
|
||||
|
||||
import pytest
|
||||
from backend.managers.system_prompter import SystemPrompter, MAX_FILE_CHARS # MAX_FILE_CHARS: character limit before content is truncated
|
||||
from backend.managers.system_prompter import SystemPrompter, MAX_FILE_CHARS
|
||||
|
||||
|
||||
class TestSystemPrompterBasePrompt:
|
||||
"""Tests for generate_prompt() without file context."""
|
||||
|
||||
def test_returns_non_empty_string(self):
|
||||
prompt = SystemPrompter.generate_prompt()
|
||||
assert isinstance(prompt, str)
|
||||
assert len(prompt) > 0
|
||||
|
||||
def test_describes_code_assistant(self):
|
||||
prompt = SystemPrompter.generate_prompt()
|
||||
assert "code assistant" in prompt.lower()
|
||||
|
||||
def test_contains_no_file_xml_tag(self):
|
||||
prompt = SystemPrompter.generate_prompt()
|
||||
assert "<file" not in prompt
|
||||
assert "<code>" not in prompt
|
||||
|
||||
def test_none_equals_no_argument(self):
|
||||
assert SystemPrompter.generate_prompt(file_context=None) == SystemPrompter.generate_prompt()
|
||||
|
||||
def test_empty_dict_behaves_like_none(self):
|
||||
# {} is falsy in Python, so the implementation treats it the same as None (no file context).
|
||||
assert SystemPrompter.generate_prompt(file_context={}) == SystemPrompter.generate_prompt()
|
||||
|
||||
|
||||
class TestSystemPrompterWithFileContext:
|
||||
"""Tests for generate_prompt() with file_context provided."""
|
||||
@ -53,10 +37,6 @@ class TestSystemPrompterWithFileContext:
|
||||
prompt = SystemPrompter.generate_prompt(file_context={"name": "f.py", "content": "pass"})
|
||||
assert "<code>" in prompt
|
||||
|
||||
def test_missing_name_key_uses_unknown(self):
|
||||
prompt = SystemPrompter.generate_prompt(file_context={"content": "some code"})
|
||||
assert "unknown" in prompt
|
||||
|
||||
def test_missing_content_key_does_not_raise(self):
|
||||
prompt = SystemPrompter.generate_prompt(file_context={"name": "empty.py"})
|
||||
assert "empty.py" in prompt
|
||||
@ -76,11 +56,6 @@ class TestSystemPrompterTruncation:
|
||||
assert "[truncated]" not in prompt
|
||||
assert content in prompt
|
||||
|
||||
def test_file_exactly_at_limit_is_not_truncated(self):
|
||||
content = "x" * MAX_FILE_CHARS
|
||||
prompt = SystemPrompter.generate_prompt(file_context={"name": "f.py", "content": content})
|
||||
assert "[truncated]" not in prompt
|
||||
|
||||
def test_file_one_over_limit_is_truncated(self):
|
||||
content = "x" * (MAX_FILE_CHARS + 1)
|
||||
prompt = SystemPrompter.generate_prompt(file_context={"name": "f.py", "content": content})
|
||||
@ -90,24 +65,6 @@ class TestSystemPrompterTruncation:
|
||||
class TestSystemPrompterSpecialCharacters:
|
||||
"""Tests that XML special characters in file content are handled without breaking the prompt."""
|
||||
|
||||
def test_less_than_in_content_does_not_break_prompt(self):
|
||||
prompt = SystemPrompter.generate_prompt(
|
||||
file_context={"name": "f.py", "content": "if x < 10:"}
|
||||
)
|
||||
assert "if x < 10:" in prompt
|
||||
|
||||
def test_greater_than_in_content_does_not_break_prompt(self):
|
||||
prompt = SystemPrompter.generate_prompt(
|
||||
file_context={"name": "f.py", "content": "if x > 0:"}
|
||||
)
|
||||
assert "if x > 0:" in prompt
|
||||
|
||||
def test_ampersand_in_content_does_not_break_prompt(self):
|
||||
prompt = SystemPrompter.generate_prompt(
|
||||
file_context={"name": "f.py", "content": "# true & false"}
|
||||
)
|
||||
assert "true & false" in prompt
|
||||
|
||||
def test_xml_tags_in_content_are_preserved_literally(self):
|
||||
# User code often contains HTML or XML. The prompt builder must embed it
|
||||
# verbatim — escaping or stripping tags would corrupt the file content.
|
||||
@ -115,12 +72,3 @@ class TestSystemPrompterSpecialCharacters:
|
||||
file_context={"name": "template.html", "content": "<div>hello</div>"}
|
||||
)
|
||||
assert "<div>hello</div>" in prompt
|
||||
|
||||
def test_prompt_still_contains_file_xml_tag_with_special_content(self):
|
||||
# Verifies that special characters in the content don't corrupt the surrounding
|
||||
# structural tags (<file ...> and <code>) that the LLM relies on for context.
|
||||
prompt = SystemPrompter.generate_prompt(
|
||||
file_context={"name": "f.py", "content": "x < y and a > b"}
|
||||
)
|
||||
assert "<file" in prompt
|
||||
assert "<code>" in prompt
|
||||
|
||||
Loading…
x
Reference in New Issue
Block a user