mirror of
https://github.com/Alishahryar1/free-claude-code.git
synced 2026-07-03 14:05:26 +02:00
Improved test coverage
This commit is contained in:
@@ -210,3 +210,146 @@ def test_app_lifespan_cleanup_continues_if_platform_stop_raises(tmp_path):
|
||||
fake_platform.stop.assert_awaited_once()
|
||||
cli_manager.stop_all.assert_awaited_once()
|
||||
cleanup_provider.assert_awaited_once()
|
||||
|
||||
|
||||
def test_app_lifespan_messaging_import_error_no_crash(tmp_path, caplog):
|
||||
"""Messaging import failure logs warning and continues without crash."""
|
||||
from api.app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
settings = SimpleNamespace(
|
||||
messaging_platform="telegram",
|
||||
telegram_bot_token="token",
|
||||
allowed_telegram_user_id="123",
|
||||
discord_bot_token=None,
|
||||
allowed_discord_channels=None,
|
||||
allowed_dir=str(tmp_path / "workspace"),
|
||||
claude_workspace=str(tmp_path / "data"),
|
||||
host="127.0.0.1",
|
||||
port=8082,
|
||||
max_cli_sessions=1,
|
||||
log_file=str(tmp_path / "server.log"),
|
||||
)
|
||||
|
||||
api_app_mod = importlib.import_module("api.app")
|
||||
cleanup_provider = AsyncMock()
|
||||
with (
|
||||
patch.object(api_app_mod, "get_settings", return_value=settings),
|
||||
patch.object(api_app_mod, "cleanup_provider", new=cleanup_provider),
|
||||
patch(
|
||||
"messaging.factory.create_messaging_platform",
|
||||
side_effect=ImportError("discord not installed"),
|
||||
),
|
||||
):
|
||||
with TestClient(app):
|
||||
pass
|
||||
|
||||
assert getattr(app.state, "messaging_platform", None) is None
|
||||
cleanup_provider.assert_awaited_once()
|
||||
|
||||
|
||||
def test_app_lifespan_platform_start_exception_cleanup_still_runs(tmp_path):
|
||||
"""Exception during platform.start() logs error, cleanup still runs."""
|
||||
from api.app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
settings = SimpleNamespace(
|
||||
messaging_platform="telegram",
|
||||
telegram_bot_token="token",
|
||||
allowed_telegram_user_id="123",
|
||||
discord_bot_token=None,
|
||||
allowed_discord_channels=None,
|
||||
allowed_dir=str(tmp_path / "workspace"),
|
||||
claude_workspace=str(tmp_path / "data"),
|
||||
host="127.0.0.1",
|
||||
port=8082,
|
||||
max_cli_sessions=1,
|
||||
log_file=str(tmp_path / "server.log"),
|
||||
)
|
||||
|
||||
fake_platform = MagicMock()
|
||||
fake_platform.name = "fake"
|
||||
fake_platform.on_message = MagicMock()
|
||||
fake_platform.start = AsyncMock(side_effect=RuntimeError("start failed"))
|
||||
fake_platform.stop = AsyncMock()
|
||||
|
||||
session_store = MagicMock()
|
||||
session_store.get_all_trees.return_value = []
|
||||
session_store.get_node_mapping.return_value = {}
|
||||
session_store.sync_from_tree_data = MagicMock()
|
||||
|
||||
cli_manager = MagicMock()
|
||||
cli_manager.stop_all = AsyncMock()
|
||||
|
||||
api_app_mod = importlib.import_module("api.app")
|
||||
cleanup_provider = AsyncMock()
|
||||
with (
|
||||
patch.object(api_app_mod, "get_settings", return_value=settings),
|
||||
patch.object(api_app_mod, "cleanup_provider", new=cleanup_provider),
|
||||
patch(
|
||||
"messaging.factory.create_messaging_platform",
|
||||
return_value=fake_platform,
|
||||
),
|
||||
patch("messaging.session.SessionStore", return_value=session_store),
|
||||
patch("cli.manager.CLISessionManager", return_value=cli_manager),
|
||||
):
|
||||
with TestClient(app):
|
||||
pass
|
||||
|
||||
cleanup_provider.assert_awaited_once()
|
||||
|
||||
|
||||
def test_app_lifespan_flush_pending_save_exception_warning_only(tmp_path):
|
||||
"""Session store flush exception on shutdown is logged as warning, no crash."""
|
||||
from api.app import create_app
|
||||
|
||||
app = create_app()
|
||||
|
||||
settings = SimpleNamespace(
|
||||
messaging_platform="telegram",
|
||||
telegram_bot_token="token",
|
||||
allowed_telegram_user_id="123",
|
||||
discord_bot_token=None,
|
||||
allowed_discord_channels=None,
|
||||
allowed_dir=str(tmp_path / "workspace"),
|
||||
claude_workspace=str(tmp_path / "data"),
|
||||
host="127.0.0.1",
|
||||
port=8082,
|
||||
max_cli_sessions=1,
|
||||
log_file=str(tmp_path / "server.log"),
|
||||
)
|
||||
|
||||
fake_platform = MagicMock()
|
||||
fake_platform.name = "fake"
|
||||
fake_platform.on_message = MagicMock()
|
||||
fake_platform.start = AsyncMock()
|
||||
fake_platform.stop = AsyncMock()
|
||||
|
||||
session_store = MagicMock()
|
||||
session_store.get_all_trees.return_value = []
|
||||
session_store.get_node_mapping.return_value = {}
|
||||
session_store.sync_from_tree_data = MagicMock()
|
||||
session_store.flush_pending_save = MagicMock(side_effect=IOError("disk full"))
|
||||
|
||||
cli_manager = MagicMock()
|
||||
cli_manager.stop_all = AsyncMock()
|
||||
|
||||
api_app_mod = importlib.import_module("api.app")
|
||||
cleanup_provider = AsyncMock()
|
||||
with (
|
||||
patch.object(api_app_mod, "get_settings", return_value=settings),
|
||||
patch.object(api_app_mod, "cleanup_provider", new=cleanup_provider),
|
||||
patch(
|
||||
"messaging.factory.create_messaging_platform",
|
||||
return_value=fake_platform,
|
||||
),
|
||||
patch("messaging.session.SessionStore", return_value=session_store),
|
||||
patch("cli.manager.CLISessionManager", return_value=cli_manager),
|
||||
):
|
||||
with TestClient(app):
|
||||
pass
|
||||
|
||||
session_store.flush_pending_save.assert_called_once()
|
||||
cleanup_provider.assert_awaited_once()
|
||||
|
||||
@@ -0,0 +1,80 @@
|
||||
"""Edge case tests for api/detection.py."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from api.detection import (
|
||||
is_prefix_detection_request,
|
||||
is_filepath_extraction_request,
|
||||
)
|
||||
from api.models.anthropic import MessagesRequest, Message
|
||||
|
||||
|
||||
def _make_request(content: str, **kwargs) -> MessagesRequest:
|
||||
return MessagesRequest(
|
||||
model="claude-3-sonnet",
|
||||
max_tokens=100,
|
||||
messages=[Message(role="user", content=content)],
|
||||
**kwargs,
|
||||
)
|
||||
|
||||
|
||||
class TestIsPrefixDetectionRequest:
|
||||
def test_output_marker_handling(self):
|
||||
"""Content with Command: but Output: after cmd_start; output has < or \\n\\n."""
|
||||
content = "<policy_spec> Command:\nls -la\nOutput:\na.txt\nb.txt\n\nmore"
|
||||
req = _make_request(content)
|
||||
is_req, cmd = is_prefix_detection_request(req)
|
||||
assert is_req is True
|
||||
assert "ls -la" in cmd
|
||||
|
||||
def test_prefix_detection_with_empty_command_section(self):
|
||||
"""Command: at end with no content returns empty command."""
|
||||
req = _make_request("<policy_spec> Command: ")
|
||||
is_req, cmd = is_prefix_detection_request(req)
|
||||
assert is_req is True
|
||||
assert cmd == ""
|
||||
|
||||
def test_exception_in_try_returns_false(self):
|
||||
"""Exception in try block (e.g. content slice) returns False, ''."""
|
||||
req = _make_request("<policy_spec> Command: x")
|
||||
|
||||
# Return object that raises when sliced - triggers except in is_prefix_detection_request
|
||||
class BadStr(str):
|
||||
def __getitem__(self, key):
|
||||
raise TypeError("bad slice")
|
||||
|
||||
with patch(
|
||||
"api.detection.extract_text_from_content",
|
||||
return_value=BadStr("<policy_spec> Command: x"),
|
||||
):
|
||||
is_req, cmd = is_prefix_detection_request(req)
|
||||
assert is_req is False
|
||||
assert cmd == ""
|
||||
|
||||
|
||||
class TestIsFilepathExtractionRequest:
|
||||
def test_output_marker_minus_one_returns_false(self):
|
||||
"""Output: not found after Command: returns False."""
|
||||
content = "Command:\nls\nfilepaths"
|
||||
req = _make_request(content)
|
||||
is_fp, cmd, out = is_filepath_extraction_request(req)
|
||||
assert is_fp is False
|
||||
assert cmd == ""
|
||||
assert out == ""
|
||||
|
||||
def test_output_has_angle_bracket_splits(self):
|
||||
"""Output containing < is split and first part used."""
|
||||
content = "Command:\nls\nOutput:\na.txt b.txt <extra>\nfilepaths"
|
||||
req = _make_request(content)
|
||||
is_fp, cmd, out = is_filepath_extraction_request(req)
|
||||
assert is_fp is True
|
||||
assert "<" not in out
|
||||
assert out == "a.txt b.txt"
|
||||
|
||||
def test_output_has_double_newline_splits(self):
|
||||
"""Output containing \\n\\n is split and first part used."""
|
||||
content = "Command:\nls\nOutput:\na.txt\nb.txt\n\nmore text\nfilepaths"
|
||||
req = _make_request(content)
|
||||
is_fp, cmd, out = is_filepath_extraction_request(req)
|
||||
assert is_fp is True
|
||||
assert "more" not in out
|
||||
@@ -0,0 +1,201 @@
|
||||
"""Tests for messaging/discord_markdown.py."""
|
||||
|
||||
from messaging.discord_markdown import (
|
||||
escape_discord,
|
||||
escape_discord_code,
|
||||
discord_bold,
|
||||
discord_code_inline,
|
||||
format_status_discord,
|
||||
format_status,
|
||||
render_markdown_to_discord,
|
||||
_is_gfm_table_header_line,
|
||||
_normalize_gfm_tables,
|
||||
)
|
||||
|
||||
|
||||
class TestEscapeDiscord:
|
||||
"""Tests for escape_discord."""
|
||||
|
||||
def test_empty_string(self):
|
||||
assert escape_discord("") == ""
|
||||
|
||||
def test_plain_text_unchanged(self):
|
||||
assert escape_discord("hello world") == "hello world"
|
||||
|
||||
def test_special_chars_escaped(self):
|
||||
for ch in "\\*_`~|>":
|
||||
assert escape_discord(ch) == f"\\{ch}"
|
||||
|
||||
def test_mixed_special_and_plain(self):
|
||||
assert escape_discord("a*b_c") == "a\\*b\\_c"
|
||||
|
||||
def test_unicode_preserved(self):
|
||||
assert escape_discord("café 日本語") == "café 日本語"
|
||||
|
||||
|
||||
class TestEscapeDiscordCode:
|
||||
"""Tests for escape_discord_code."""
|
||||
|
||||
def test_empty_string(self):
|
||||
assert escape_discord_code("") == ""
|
||||
|
||||
def test_backslash_escaped(self):
|
||||
assert escape_discord_code("\\") == "\\\\"
|
||||
|
||||
def test_backtick_escaped(self):
|
||||
assert escape_discord_code("`") == "\\`"
|
||||
|
||||
def test_both_escaped(self):
|
||||
assert escape_discord_code("`\\") == "\\`\\\\"
|
||||
|
||||
|
||||
class TestDiscordBold:
|
||||
"""Tests for discord_bold."""
|
||||
|
||||
def test_simple(self):
|
||||
assert discord_bold("hello") == "**hello**"
|
||||
|
||||
def test_escapes_inner(self):
|
||||
assert discord_bold("a*b") == "**a\\*b**"
|
||||
|
||||
|
||||
class TestDiscordCodeInline:
|
||||
"""Tests for discord_code_inline."""
|
||||
|
||||
def test_simple(self):
|
||||
assert discord_code_inline("x") == "`x`"
|
||||
|
||||
def test_escapes_backtick(self):
|
||||
assert discord_code_inline("`") == "`\\``"
|
||||
|
||||
|
||||
class TestFormatStatusDiscord:
|
||||
"""Tests for format_status_discord."""
|
||||
|
||||
def test_label_only(self):
|
||||
assert format_status_discord("Running") == "**Running**"
|
||||
|
||||
def test_label_with_suffix(self):
|
||||
# Parentheses not in DISCORD_SPECIAL, so unchanged
|
||||
assert (
|
||||
format_status_discord("Queued", "(position 2)") == "**Queued** (position 2)"
|
||||
)
|
||||
|
||||
|
||||
class TestFormatStatus:
|
||||
"""Tests for format_status."""
|
||||
|
||||
def test_label_only(self):
|
||||
assert format_status("🔄", "Running") == "🔄 **Running**"
|
||||
|
||||
def test_label_with_suffix(self):
|
||||
assert format_status("⏳", "Waiting", "5/10") == "⏳ **Waiting** 5/10"
|
||||
|
||||
|
||||
class TestIsGfmTableHeaderLine:
|
||||
"""Tests for _is_gfm_table_header_line."""
|
||||
|
||||
def test_no_pipe_returns_false(self):
|
||||
assert _is_gfm_table_header_line("hello world") is False
|
||||
|
||||
def test_separator_only_returns_false(self):
|
||||
assert _is_gfm_table_header_line("|---|") is False
|
||||
assert _is_gfm_table_header_line("|:---|:---|") is False
|
||||
|
||||
def test_valid_header(self):
|
||||
assert _is_gfm_table_header_line("| A | B |") is True
|
||||
assert _is_gfm_table_header_line("A | B") is True
|
||||
|
||||
def test_single_column_returns_false(self):
|
||||
assert _is_gfm_table_header_line("| A |") is False
|
||||
|
||||
|
||||
class TestNormalizeGfmTables:
|
||||
"""Tests for _normalize_gfm_tables."""
|
||||
|
||||
def test_single_line_unchanged(self):
|
||||
assert _normalize_gfm_tables("hello") == "hello"
|
||||
|
||||
def test_two_lines_no_table_unchanged(self):
|
||||
assert _normalize_gfm_tables("a\nb") == "a\nb"
|
||||
|
||||
def test_table_gets_blank_line_before(self):
|
||||
text = "para\n| A | B |\n|---|\n| 1 | 2 |"
|
||||
result = _normalize_gfm_tables(text)
|
||||
assert "para" in result
|
||||
assert "| A | B |" in result
|
||||
|
||||
def test_table_inside_fence_unchanged(self):
|
||||
text = "```\n| A | B |\n|---|\n```"
|
||||
result = _normalize_gfm_tables(text)
|
||||
assert result == text
|
||||
|
||||
|
||||
class TestRenderMarkdownToDiscord:
|
||||
"""Tests for render_markdown_to_discord."""
|
||||
|
||||
def test_empty_string(self):
|
||||
assert render_markdown_to_discord("") == ""
|
||||
|
||||
def test_plain_paragraph(self):
|
||||
assert "hello" in render_markdown_to_discord("hello")
|
||||
|
||||
def test_headings(self):
|
||||
result = render_markdown_to_discord("# Title\n## Sub")
|
||||
assert "Title" in result
|
||||
assert "Sub" in result
|
||||
|
||||
def test_bold_italic(self):
|
||||
result = render_markdown_to_discord("**bold** *italic*")
|
||||
assert "bold" in result
|
||||
assert "italic" in result
|
||||
|
||||
def test_strikethrough(self):
|
||||
result = render_markdown_to_discord("~~strike~~")
|
||||
assert "strike" in result
|
||||
|
||||
def test_inline_code(self):
|
||||
result = render_markdown_to_discord("use `code` here")
|
||||
assert "`" in result
|
||||
assert "code" in result
|
||||
|
||||
def test_code_block(self):
|
||||
result = render_markdown_to_discord("```\nprint(1)\n```")
|
||||
assert "print(1)" in result
|
||||
assert "```" in result
|
||||
|
||||
def test_blockquote(self):
|
||||
result = render_markdown_to_discord("> quote")
|
||||
assert "quote" in result
|
||||
|
||||
def test_bullet_list(self):
|
||||
result = render_markdown_to_discord("- a\n- b")
|
||||
assert "a" in result
|
||||
assert "b" in result
|
||||
|
||||
def test_ordered_list(self):
|
||||
result = render_markdown_to_discord("1. first\n2. second")
|
||||
assert "first" in result
|
||||
assert "second" in result
|
||||
|
||||
def test_link(self):
|
||||
result = render_markdown_to_discord("[text](https://example.com)")
|
||||
assert "text" in result
|
||||
assert "https://example.com" in result
|
||||
|
||||
def test_image_with_alt(self):
|
||||
result = render_markdown_to_discord("")
|
||||
assert "alt" in result
|
||||
assert "https://img.png" in result
|
||||
|
||||
def test_image_without_alt(self):
|
||||
result = render_markdown_to_discord("")
|
||||
assert "https://img.png" in result
|
||||
|
||||
def test_gfm_table(self):
|
||||
text = "| A | B |\n|---|---|\n| 1 | 2 |"
|
||||
result = render_markdown_to_discord(text)
|
||||
assert "A" in result
|
||||
assert "B" in result
|
||||
assert "1" in result
|
||||
assert "2" in result
|
||||
@@ -1,15 +1,29 @@
|
||||
"""Tests for Discord platform adapter."""
|
||||
|
||||
import asyncio
|
||||
import pytest
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
from messaging.discord import (
|
||||
DiscordPlatform,
|
||||
_parse_allowed_channels,
|
||||
_get_discord,
|
||||
DISCORD_AVAILABLE,
|
||||
)
|
||||
|
||||
|
||||
class TestGetDiscord:
|
||||
"""Tests for _get_discord helper."""
|
||||
|
||||
def test_raises_when_discord_not_available(self):
|
||||
import messaging.discord as discord_mod
|
||||
|
||||
with patch.object(discord_mod, "DISCORD_AVAILABLE", False):
|
||||
with patch.object(discord_mod, "_discord_module", None):
|
||||
with pytest.raises(ImportError, match="discord.py is required"):
|
||||
_get_discord()
|
||||
|
||||
|
||||
class TestParseAllowedChannels:
|
||||
"""Tests for _parse_allowed_channels helper."""
|
||||
|
||||
@@ -17,6 +31,9 @@ class TestParseAllowedChannels:
|
||||
assert _parse_allowed_channels("") == set()
|
||||
assert _parse_allowed_channels(None) == set()
|
||||
|
||||
def test_whitespace_only_returns_empty_set(self):
|
||||
assert _parse_allowed_channels(" ") == set()
|
||||
|
||||
def test_single_channel(self):
|
||||
assert _parse_allowed_channels("123456789") == {"123456789"}
|
||||
|
||||
@@ -66,6 +83,22 @@ class TestDiscordPlatform:
|
||||
short = "hello"
|
||||
assert platform._truncate(short) == short
|
||||
|
||||
def test_truncate_exactly_at_limit_unchanged(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
exact = "x" * 2000
|
||||
assert platform._truncate(exact) == exact
|
||||
|
||||
def test_truncate_one_over_limit_truncates(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
over = "x" * 2001
|
||||
result = platform._truncate(over)
|
||||
assert len(result) == 2000
|
||||
assert result.endswith("...")
|
||||
|
||||
def test_truncate_empty_string(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
assert platform._truncate("") == ""
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_returns_message_id(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
@@ -92,3 +125,237 @@ class TestDiscordPlatform:
|
||||
):
|
||||
await platform.edit_message("123", "456", "Updated text")
|
||||
mock_msg.edit.assert_called_once_with(content="Updated text")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_channel_not_found_raises(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
platform._connected = True
|
||||
with patch.object(
|
||||
platform._client, "get_channel", MagicMock(return_value=None)
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="Channel"):
|
||||
await platform.send_message("123", "Hello")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_channel_no_send_raises(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
platform._connected = True
|
||||
mock_channel = MagicMock(spec=[]) # No send attr
|
||||
with patch.object(
|
||||
platform._client, "get_channel", MagicMock(return_value=mock_channel)
|
||||
):
|
||||
with pytest.raises(RuntimeError, match="Channel"):
|
||||
await platform.send_message("123", "Hello")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_queue_send_message_without_limiter_calls_send_message(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
platform._limiter = None
|
||||
platform._connected = True
|
||||
mock_channel = AsyncMock()
|
||||
mock_msg = MagicMock()
|
||||
mock_msg.id = 42
|
||||
mock_channel.send = AsyncMock(return_value=mock_msg)
|
||||
with patch.object(
|
||||
platform._client, "get_channel", MagicMock(return_value=mock_channel)
|
||||
):
|
||||
result = await platform.queue_send_message("123", "hi")
|
||||
assert result == "42"
|
||||
mock_channel.send.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_queue_edit_message_without_limiter_calls_edit_message(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
platform._limiter = None
|
||||
platform._connected = True
|
||||
mock_msg = AsyncMock()
|
||||
mock_channel = AsyncMock()
|
||||
mock_channel.fetch_message = AsyncMock(return_value=mock_msg)
|
||||
with patch.object(
|
||||
platform._client, "get_channel", MagicMock(return_value=mock_channel)
|
||||
):
|
||||
await platform.queue_edit_message("123", "456", "Updated")
|
||||
mock_msg.edit.assert_called_once_with(content="Updated")
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_discord_message_bot_ignored(self):
|
||||
platform = DiscordPlatform(bot_token="token", allowed_channel_ids="123")
|
||||
handler = AsyncMock()
|
||||
platform.on_message(handler)
|
||||
msg = MagicMock()
|
||||
msg.author.bot = True
|
||||
msg.content = "hello"
|
||||
msg.channel.id = 123
|
||||
await platform._on_discord_message(msg)
|
||||
handler.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_discord_message_empty_content_ignored(self):
|
||||
platform = DiscordPlatform(bot_token="token", allowed_channel_ids="123")
|
||||
handler = AsyncMock()
|
||||
platform.on_message(handler)
|
||||
msg = MagicMock()
|
||||
msg.author.bot = False
|
||||
msg.content = ""
|
||||
msg.channel.id = 123
|
||||
await platform._on_discord_message(msg)
|
||||
handler.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_discord_message_channel_not_allowed_ignored(self):
|
||||
platform = DiscordPlatform(bot_token="token", allowed_channel_ids="123")
|
||||
handler = AsyncMock()
|
||||
platform.on_message(handler)
|
||||
msg = MagicMock()
|
||||
msg.author.bot = False
|
||||
msg.content = "hello"
|
||||
msg.channel.id = 999
|
||||
await platform._on_discord_message(msg)
|
||||
handler.assert_not_called()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_on_discord_message_valid_calls_handler(self):
|
||||
platform = DiscordPlatform(bot_token="token", allowed_channel_ids="123")
|
||||
handler = AsyncMock()
|
||||
platform.on_message(handler)
|
||||
msg = MagicMock()
|
||||
msg.author.bot = False
|
||||
msg.author.id = 456
|
||||
msg.author.display_name = "User"
|
||||
msg.content = "hello"
|
||||
msg.channel.id = 123
|
||||
msg.id = 789
|
||||
msg.reference = None
|
||||
await platform._on_discord_message(msg)
|
||||
handler.assert_awaited_once()
|
||||
call = handler.call_args[0][0]
|
||||
assert call.text == "hello"
|
||||
assert call.chat_id == "123"
|
||||
assert call.user_id == "456"
|
||||
assert call.message_id == "789"
|
||||
assert call.platform == "discord"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_send_message_with_reply_to(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
mock_msg = MagicMock()
|
||||
mock_msg.id = 999
|
||||
mock_channel = AsyncMock()
|
||||
mock_channel.send = AsyncMock(return_value=mock_msg)
|
||||
platform._connected = True
|
||||
with patch.object(
|
||||
platform._client, "get_channel", MagicMock(return_value=mock_channel)
|
||||
):
|
||||
with patch("messaging.discord._get_discord") as mock_get:
|
||||
mock_discord = MagicMock()
|
||||
mock_get.return_value = mock_discord
|
||||
msg_id = await platform.send_message("123", "Hello", reply_to="456")
|
||||
assert msg_id == "999"
|
||||
mock_channel.send.assert_awaited_once()
|
||||
call_kw = mock_channel.send.call_args[1]
|
||||
assert call_kw.get("reference") is not None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_edit_message_not_found_returns_gracefully(self):
|
||||
import discord as discord_pkg
|
||||
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
mock_channel = AsyncMock()
|
||||
mock_resp = MagicMock()
|
||||
mock_resp.status = 404
|
||||
mock_channel.fetch_message = AsyncMock(
|
||||
side_effect=discord_pkg.NotFound(mock_resp, "Not found")
|
||||
)
|
||||
platform._connected = True
|
||||
with patch.object(
|
||||
platform._client, "get_channel", MagicMock(return_value=mock_channel)
|
||||
):
|
||||
await platform.edit_message("123", "456", "Updated")
|
||||
# Should not raise - NotFound is caught and we return
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_delete_message(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
mock_msg = AsyncMock()
|
||||
mock_channel = AsyncMock()
|
||||
mock_channel.fetch_message = AsyncMock(return_value=mock_msg)
|
||||
platform._connected = True
|
||||
with patch.object(
|
||||
platform._client, "get_channel", MagicMock(return_value=mock_channel)
|
||||
):
|
||||
with patch("messaging.discord._get_discord") as mock_get:
|
||||
mock_get.return_value = MagicMock()
|
||||
await platform.delete_message("123", "456")
|
||||
mock_msg.delete.assert_awaited_once()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_fire_and_forget_with_coroutine(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
|
||||
async def _task():
|
||||
pass
|
||||
|
||||
coro = _task()
|
||||
with patch("asyncio.create_task") as mock_create:
|
||||
|
||||
def _run(c):
|
||||
return asyncio.ensure_future(c)
|
||||
|
||||
mock_create.side_effect = _run
|
||||
platform.fire_and_forget(coro)
|
||||
mock_create.assert_called_once()
|
||||
await asyncio.sleep(0)
|
||||
|
||||
def test_on_message_registers_handler(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
handler = AsyncMock()
|
||||
platform.on_message(handler)
|
||||
assert platform._message_handler is handler
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_requires_token(self):
|
||||
with patch.dict("os.environ", {"DISCORD_BOT_TOKEN": ""}, clear=False):
|
||||
platform = DiscordPlatform(bot_token="")
|
||||
with pytest.raises(ValueError, match="DISCORD_BOT_TOKEN"):
|
||||
await platform.start()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_start_connects(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
|
||||
async def _fake_start(_token):
|
||||
platform._connected = True
|
||||
|
||||
with patch.object(
|
||||
platform._client, "start", new_callable=AsyncMock, side_effect=_fake_start
|
||||
):
|
||||
with patch(
|
||||
"messaging.limiter.MessagingRateLimiter.get_instance",
|
||||
new_callable=AsyncMock,
|
||||
):
|
||||
await platform.start()
|
||||
assert platform.is_connected is True
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_when_already_closed(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
platform._connected = True
|
||||
with patch.object(
|
||||
platform._client, "is_closed", new_callable=MagicMock, return_value=True
|
||||
):
|
||||
await platform.stop()
|
||||
assert platform.is_connected is False
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stop_closes_client(self):
|
||||
platform = DiscordPlatform(bot_token="token")
|
||||
platform._connected = True
|
||||
mock_close = AsyncMock()
|
||||
with patch.object(
|
||||
platform._client, "is_closed", new_callable=MagicMock, return_value=False
|
||||
):
|
||||
with patch.object(platform._client, "close", mock_close):
|
||||
platform._start_task = None
|
||||
await platform.stop()
|
||||
mock_close.assert_awaited_once()
|
||||
assert platform.is_connected is False
|
||||
|
||||
@@ -11,6 +11,47 @@ def handler(mock_platform, mock_cli_manager, mock_session_store):
|
||||
return ClaudeMessageHandler(mock_platform, mock_cli_manager, mock_session_store)
|
||||
|
||||
|
||||
def test_get_initial_status_new_conversation_with_slot(handler):
|
||||
"""New conversation when slots available returns launching message."""
|
||||
handler.cli_manager.get_stats.return_value = {
|
||||
"active_sessions": 0,
|
||||
"max_sessions": 5,
|
||||
}
|
||||
result = handler._get_initial_status(None, None)
|
||||
assert "Launching" in result
|
||||
|
||||
|
||||
def test_get_initial_status_new_conversation_at_capacity(handler):
|
||||
"""New conversation at capacity returns waiting message."""
|
||||
handler.cli_manager.get_stats.return_value = {
|
||||
"active_sessions": 5,
|
||||
"max_sessions": 5,
|
||||
}
|
||||
result = handler._get_initial_status(None, None)
|
||||
assert "Waiting" in result
|
||||
assert "5/5" in result
|
||||
|
||||
|
||||
def test_get_initial_status_reply_tree_busy_queued(handler):
|
||||
"""Reply to tree when busy returns queued message."""
|
||||
mock_queue = MagicMock()
|
||||
mock_queue.is_node_tree_busy.return_value = True
|
||||
mock_queue.get_queue_size.return_value = 2
|
||||
handler.tree_queue = mock_queue
|
||||
result = handler._get_initial_status(MagicMock(), "parent_1")
|
||||
assert "Queued" in result
|
||||
assert "position 3" in result
|
||||
|
||||
|
||||
def test_get_initial_status_reply_tree_not_busy_continuing(handler):
|
||||
"""Reply to tree when not busy returns continuing message."""
|
||||
mock_queue = MagicMock()
|
||||
mock_queue.is_node_tree_busy.return_value = False
|
||||
handler.tree_queue = mock_queue
|
||||
result = handler._get_initial_status(MagicMock(), "parent_1")
|
||||
assert "Continuing" in result
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_handle_message_stop_command(
|
||||
handler, mock_platform, incoming_message_factory
|
||||
|
||||
@@ -0,0 +1,101 @@
|
||||
"""Tests for providers/nvidia_nim/request.py."""
|
||||
|
||||
from unittest.mock import MagicMock
|
||||
|
||||
from providers.nvidia_nim.request import (
|
||||
_set_if_not_none,
|
||||
_set_extra,
|
||||
build_request_body,
|
||||
)
|
||||
from config.nim import NimSettings
|
||||
|
||||
|
||||
class TestSetIfNotNone:
|
||||
def test_value_not_none_sets(self):
|
||||
body = {}
|
||||
_set_if_not_none(body, "key", "value")
|
||||
assert body["key"] == "value"
|
||||
|
||||
def test_value_none_skips(self):
|
||||
body = {}
|
||||
_set_if_not_none(body, "key", None)
|
||||
assert "key" not in body
|
||||
|
||||
|
||||
class TestSetExtra:
|
||||
def test_key_in_extra_body_skips(self):
|
||||
extra = {"top_k": 42}
|
||||
_set_extra(extra, "top_k", 10)
|
||||
assert extra["top_k"] == 42
|
||||
|
||||
def test_value_none_skips(self):
|
||||
extra = {}
|
||||
_set_extra(extra, "top_k", None)
|
||||
assert "top_k" not in extra
|
||||
|
||||
def test_value_equals_ignore_value_skips(self):
|
||||
extra = {}
|
||||
_set_extra(extra, "top_k", -1, ignore_value=-1)
|
||||
assert "top_k" not in extra
|
||||
|
||||
def test_value_set_when_valid(self):
|
||||
extra = {}
|
||||
_set_extra(extra, "top_k", 10, ignore_value=-1)
|
||||
assert extra["top_k"] == 10
|
||||
|
||||
|
||||
class TestBuildRequestBody:
|
||||
def test_max_tokens_capped_by_nim(self):
|
||||
"""Request max_tokens exceeds nim.max_tokens -> capped."""
|
||||
req = MagicMock()
|
||||
req.model = "test"
|
||||
req.messages = [MagicMock(role="user", content="hi")]
|
||||
req.max_tokens = 100000
|
||||
req.system = None
|
||||
req.temperature = None
|
||||
req.top_p = None
|
||||
req.stop_sequences = None
|
||||
req.tools = None
|
||||
req.tool_choice = None
|
||||
req.extra_body = None
|
||||
req.top_k = None
|
||||
|
||||
nim = NimSettings(max_tokens=4096)
|
||||
body = build_request_body(req, nim)
|
||||
assert body["max_tokens"] == 4096
|
||||
|
||||
def test_presence_penalty_included_when_nonzero(self):
|
||||
req = MagicMock()
|
||||
req.model = "test"
|
||||
req.messages = [MagicMock(role="user", content="hi")]
|
||||
req.max_tokens = 100
|
||||
req.system = None
|
||||
req.temperature = None
|
||||
req.top_p = None
|
||||
req.stop_sequences = None
|
||||
req.tools = None
|
||||
req.tool_choice = None
|
||||
req.extra_body = None
|
||||
req.top_k = None
|
||||
|
||||
nim = NimSettings(presence_penalty=0.5)
|
||||
body = build_request_body(req, nim)
|
||||
assert body["presence_penalty"] == 0.5
|
||||
|
||||
def test_parallel_tool_calls_included(self):
|
||||
req = MagicMock()
|
||||
req.model = "test"
|
||||
req.messages = [MagicMock(role="user", content="hi")]
|
||||
req.max_tokens = 100
|
||||
req.system = None
|
||||
req.temperature = None
|
||||
req.top_p = None
|
||||
req.stop_sequences = None
|
||||
req.tools = None
|
||||
req.tool_choice = None
|
||||
req.extra_body = None
|
||||
req.top_k = None
|
||||
|
||||
nim = NimSettings(parallel_tool_calls=False)
|
||||
body = build_request_body(req, nim)
|
||||
assert body["parallel_tool_calls"] is False
|
||||
@@ -204,3 +204,152 @@ async def test_stream_response_reasoning_content(open_router_provider):
|
||||
if "Thinking..." in e:
|
||||
found_thinking = True
|
||||
assert found_thinking
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response_empty_choices_skipped(open_router_provider):
|
||||
"""Chunks with empty choices are skipped."""
|
||||
req = MockRequest()
|
||||
|
||||
async def mock_stream():
|
||||
yield MagicMock(choices=[], usage=None)
|
||||
yield MagicMock(
|
||||
choices=[
|
||||
MagicMock(
|
||||
delta=MagicMock(content="ok", reasoning_content=None),
|
||||
finish_reason="stop",
|
||||
)
|
||||
],
|
||||
usage=MagicMock(completion_tokens=2),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
open_router_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
events = []
|
||||
async for event in open_router_provider.stream_response(req):
|
||||
events.append(event)
|
||||
assert any("content_block_delta" in e and "ok" in e for e in events)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response_delta_none_skipped(open_router_provider):
|
||||
"""Chunks with delta=None are skipped."""
|
||||
req = MockRequest()
|
||||
|
||||
async def mock_stream():
|
||||
yield MagicMock(
|
||||
choices=[MagicMock(delta=None, finish_reason=None)],
|
||||
usage=None,
|
||||
)
|
||||
yield MagicMock(
|
||||
choices=[
|
||||
MagicMock(
|
||||
delta=MagicMock(content="x", reasoning_content=None),
|
||||
finish_reason="stop",
|
||||
)
|
||||
],
|
||||
usage=MagicMock(completion_tokens=1),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
open_router_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
events = []
|
||||
async for event in open_router_provider.stream_response(req):
|
||||
events.append(event)
|
||||
assert any("x" in e for e in events)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response_reasoning_details(open_router_provider):
|
||||
"""Streaming with reasoning_details (stepfun format)."""
|
||||
req = MockRequest()
|
||||
|
||||
mock_chunk = MagicMock()
|
||||
mock_chunk.choices = [
|
||||
MagicMock(
|
||||
delta=MagicMock(
|
||||
content=None,
|
||||
reasoning_content=None,
|
||||
reasoning_details=[{"text": "Step 1"}],
|
||||
),
|
||||
finish_reason=None,
|
||||
)
|
||||
]
|
||||
mock_chunk.usage = None
|
||||
|
||||
async def mock_stream():
|
||||
yield mock_chunk
|
||||
yield MagicMock(
|
||||
choices=[
|
||||
MagicMock(
|
||||
delta=MagicMock(
|
||||
content=None,
|
||||
reasoning_content=None,
|
||||
reasoning_details=None,
|
||||
),
|
||||
finish_reason="stop",
|
||||
)
|
||||
],
|
||||
usage=MagicMock(completion_tokens=5),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
open_router_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
events = []
|
||||
async for event in open_router_provider.stream_response(req):
|
||||
events.append(event)
|
||||
assert any("Step 1" in e for e in events)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response_error_path(open_router_provider):
|
||||
"""Stream raises exception -> error event emitted."""
|
||||
req = MockRequest()
|
||||
|
||||
async def mock_stream():
|
||||
raise RuntimeError("API failed")
|
||||
yield # unreachable, makes it a generator
|
||||
|
||||
with patch.object(
|
||||
open_router_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
events = []
|
||||
async for event in open_router_provider.stream_response(req):
|
||||
events.append(event)
|
||||
# Error is emitted; message_stop/done indicates stream completed
|
||||
assert any("API failed" in e for e in events)
|
||||
assert any("message_stop" in e for e in events)
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_stream_response_finish_reason_only(open_router_provider):
|
||||
"""Chunk with finish_reason but no content still completes."""
|
||||
req = MockRequest()
|
||||
|
||||
async def mock_stream():
|
||||
yield MagicMock(
|
||||
choices=[
|
||||
MagicMock(
|
||||
delta=MagicMock(content=None, reasoning_content=None),
|
||||
finish_reason="stop",
|
||||
)
|
||||
],
|
||||
usage=MagicMock(completion_tokens=0),
|
||||
)
|
||||
|
||||
with patch.object(
|
||||
open_router_provider._client.chat.completions, "create", new_callable=AsyncMock
|
||||
) as mock_create:
|
||||
mock_create.return_value = mock_stream()
|
||||
events = []
|
||||
async for event in open_router_provider.stream_response(req):
|
||||
events.append(event)
|
||||
assert any("message_delta" in e for e in events)
|
||||
assert any("message_stop" in e for e in events)
|
||||
|
||||
@@ -0,0 +1,219 @@
|
||||
"""Tests for api/optimization_handlers.py."""
|
||||
|
||||
from unittest.mock import patch
|
||||
|
||||
from api.optimization_handlers import (
|
||||
try_prefix_detection,
|
||||
try_quota_mock,
|
||||
try_title_skip,
|
||||
try_suggestion_skip,
|
||||
try_filepath_mock,
|
||||
try_optimizations,
|
||||
)
|
||||
from api.models.anthropic import MessagesRequest, Message, ContentBlockText
|
||||
from config.settings import Settings
|
||||
|
||||
|
||||
def _make_request(
|
||||
messages_content: str, max_tokens: int | None = None
|
||||
) -> MessagesRequest:
|
||||
"""Create a MessagesRequest with a single user message."""
|
||||
return MessagesRequest(
|
||||
model="claude-3-sonnet",
|
||||
max_tokens=max_tokens if max_tokens is not None else 100,
|
||||
messages=[Message(role="user", content=messages_content)],
|
||||
)
|
||||
|
||||
|
||||
class TestTryPrefixDetection:
|
||||
def test_disabled_returns_none(self):
|
||||
settings = Settings()
|
||||
settings.fast_prefix_detection = False
|
||||
req = _make_request("x")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_prefix_detection_request",
|
||||
return_value=(True, "/ask"),
|
||||
):
|
||||
assert try_prefix_detection(req, settings) is None
|
||||
|
||||
def test_enabled_and_match_returns_response(self):
|
||||
settings = Settings()
|
||||
settings.fast_prefix_detection = True
|
||||
req = _make_request("x")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_prefix_detection_request",
|
||||
return_value=(True, "/ask"),
|
||||
):
|
||||
with patch(
|
||||
"api.optimization_handlers.extract_command_prefix",
|
||||
return_value="/ask",
|
||||
):
|
||||
result = try_prefix_detection(req, settings)
|
||||
assert result is not None
|
||||
block = result.content[0]
|
||||
assert isinstance(block, ContentBlockText)
|
||||
assert block.text == "/ask"
|
||||
|
||||
def test_enabled_but_no_match_returns_none(self):
|
||||
settings = Settings()
|
||||
settings.fast_prefix_detection = True
|
||||
req = _make_request("x")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_prefix_detection_request",
|
||||
return_value=(False, ""),
|
||||
):
|
||||
assert try_prefix_detection(req, settings) is None
|
||||
|
||||
|
||||
class TestTryQuotaMock:
|
||||
def test_disabled_returns_none(self):
|
||||
settings = Settings()
|
||||
settings.enable_network_probe_mock = False
|
||||
req = _make_request("quota", max_tokens=1)
|
||||
with patch(
|
||||
"api.optimization_handlers.is_quota_check_request",
|
||||
return_value=True,
|
||||
):
|
||||
assert try_quota_mock(req, settings) is None
|
||||
|
||||
def test_enabled_and_match_returns_response(self):
|
||||
settings = Settings()
|
||||
settings.enable_network_probe_mock = True
|
||||
req = _make_request("quota", max_tokens=1)
|
||||
with patch(
|
||||
"api.optimization_handlers.is_quota_check_request",
|
||||
return_value=True,
|
||||
):
|
||||
result = try_quota_mock(req, settings)
|
||||
assert result is not None
|
||||
block = result.content[0]
|
||||
assert isinstance(block, ContentBlockText)
|
||||
assert "Quota check passed" in block.text
|
||||
|
||||
|
||||
class TestTryTitleSkip:
|
||||
def test_disabled_returns_none(self):
|
||||
settings = Settings()
|
||||
settings.enable_title_generation_skip = False
|
||||
req = _make_request("write a 5-10 word title")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_title_generation_request",
|
||||
return_value=True,
|
||||
):
|
||||
assert try_title_skip(req, settings) is None
|
||||
|
||||
def test_enabled_and_match_returns_response(self):
|
||||
settings = Settings()
|
||||
settings.enable_title_generation_skip = True
|
||||
req = _make_request("x")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_title_generation_request",
|
||||
return_value=True,
|
||||
):
|
||||
result = try_title_skip(req, settings)
|
||||
assert result is not None
|
||||
block = result.content[0]
|
||||
assert isinstance(block, ContentBlockText)
|
||||
assert block.text == "Conversation"
|
||||
|
||||
|
||||
class TestTrySuggestionSkip:
|
||||
def test_disabled_returns_none(self):
|
||||
settings = Settings()
|
||||
settings.enable_suggestion_mode_skip = False
|
||||
req = _make_request("[SUGGESTION MODE: x]")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_suggestion_mode_request",
|
||||
return_value=True,
|
||||
):
|
||||
assert try_suggestion_skip(req, settings) is None
|
||||
|
||||
def test_enabled_and_match_returns_response(self):
|
||||
settings = Settings()
|
||||
settings.enable_suggestion_mode_skip = True
|
||||
req = _make_request("x")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_suggestion_mode_request",
|
||||
return_value=True,
|
||||
):
|
||||
result = try_suggestion_skip(req, settings)
|
||||
assert result is not None
|
||||
block = result.content[0]
|
||||
assert isinstance(block, ContentBlockText)
|
||||
assert block.text == ""
|
||||
|
||||
|
||||
class TestTryFilepathMock:
|
||||
def test_disabled_returns_none(self):
|
||||
settings = Settings()
|
||||
settings.enable_filepath_extraction_mock = False
|
||||
req = _make_request("Command:\nls\nOutput:\nfilepaths")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_filepath_extraction_request",
|
||||
return_value=(True, "ls", "out"),
|
||||
):
|
||||
assert try_filepath_mock(req, settings) is None
|
||||
|
||||
def test_enabled_and_match_returns_response(self):
|
||||
settings = Settings()
|
||||
settings.enable_filepath_extraction_mock = True
|
||||
req = _make_request("x")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_filepath_extraction_request",
|
||||
return_value=(True, "ls", "a.txt b.txt"),
|
||||
):
|
||||
with patch(
|
||||
"api.optimization_handlers.extract_filepaths_from_command",
|
||||
return_value="a.txt\nb.txt",
|
||||
):
|
||||
result = try_filepath_mock(req, settings)
|
||||
assert result is not None
|
||||
block = result.content[0]
|
||||
assert isinstance(block, ContentBlockText)
|
||||
assert block.text == "a.txt\nb.txt"
|
||||
|
||||
def test_extract_filepaths_empty_list_still_returns_response(self):
|
||||
settings = Settings()
|
||||
settings.enable_filepath_extraction_mock = True
|
||||
req = _make_request("x")
|
||||
with patch(
|
||||
"api.optimization_handlers.is_filepath_extraction_request",
|
||||
return_value=(True, "ls", "out"),
|
||||
):
|
||||
with patch(
|
||||
"api.optimization_handlers.extract_filepaths_from_command",
|
||||
return_value="",
|
||||
):
|
||||
result = try_filepath_mock(req, settings)
|
||||
assert result is not None
|
||||
block = result.content[0]
|
||||
assert isinstance(block, ContentBlockText)
|
||||
assert block.text == ""
|
||||
|
||||
|
||||
class TestTryOptimizations:
|
||||
def test_first_match_wins(self):
|
||||
"""Quota mock is first in OPTIMIZATION_HANDLERS; it should win over prefix."""
|
||||
settings = Settings()
|
||||
settings.enable_network_probe_mock = True
|
||||
settings.fast_prefix_detection = True
|
||||
req = _make_request("quota", max_tokens=1)
|
||||
with patch(
|
||||
"api.optimization_handlers.is_quota_check_request",
|
||||
return_value=True,
|
||||
):
|
||||
result = try_optimizations(req, settings)
|
||||
assert result is not None
|
||||
block = result.content[0]
|
||||
assert isinstance(block, ContentBlockText)
|
||||
assert "Quota check passed" in block.text
|
||||
|
||||
def test_no_match_returns_none(self):
|
||||
settings = Settings()
|
||||
settings.fast_prefix_detection = False
|
||||
settings.enable_network_probe_mock = False
|
||||
settings.enable_title_generation_skip = False
|
||||
settings.enable_suggestion_mode_skip = False
|
||||
settings.enable_filepath_extraction_mock = False
|
||||
req = _make_request("random user message")
|
||||
assert try_optimizations(req, settings) is None
|
||||
@@ -1,4 +1,48 @@
|
||||
import os
|
||||
from unittest.mock import patch
|
||||
|
||||
|
||||
def test_process_registry_register_pid_zero_noop():
|
||||
"""register_pid(0) is a no-op (early return)."""
|
||||
from cli import process_registry as pr
|
||||
|
||||
before = len(pr._pids)
|
||||
pr.register_pid(0)
|
||||
assert len(pr._pids) == before
|
||||
|
||||
|
||||
def test_process_registry_unregister_pid_zero_noop():
|
||||
"""unregister_pid(0) is a no-op."""
|
||||
from cli import process_registry as pr
|
||||
|
||||
pr.register_pid(99999)
|
||||
pr.unregister_pid(0)
|
||||
assert 99999 in pr._pids
|
||||
pr.unregister_pid(99999)
|
||||
|
||||
|
||||
def test_process_registry_ensure_atexit_idempotent():
|
||||
"""Second call to ensure_atexit_registered is idempotent."""
|
||||
from cli import process_registry as pr
|
||||
|
||||
pr.ensure_atexit_registered()
|
||||
pr.ensure_atexit_registered()
|
||||
# Should not raise; atexit handler registered once
|
||||
|
||||
|
||||
def test_process_registry_kill_all_exception_logged_no_raise(monkeypatch):
|
||||
"""Exception in os.kill/taskkill is logged but does not raise."""
|
||||
from cli import process_registry as pr
|
||||
|
||||
monkeypatch.setattr(pr, "_pids", {99999})
|
||||
monkeypatch.setattr(os, "name", "posix", raising=False)
|
||||
|
||||
def _kill_raises(pid, sig):
|
||||
raise ProcessLookupError("no such process")
|
||||
|
||||
with patch("os.kill", _kill_raises):
|
||||
pr.kill_all_best_effort()
|
||||
# Should not raise
|
||||
|
||||
|
||||
def test_process_registry_register_unregister_does_not_crash():
|
||||
|
||||
@@ -183,3 +183,72 @@ class TestProviderRateLimiter:
|
||||
f"Rolling window violated at i={i}: "
|
||||
f"dt={acquired[i + rate_limit] - acquired[i]:.3f}s"
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_init_rate_limit_zero_raises(self):
|
||||
"""rate_limit <= 0 raises ValueError."""
|
||||
GlobalRateLimiter.reset_instance()
|
||||
with pytest.raises(ValueError, match="rate_limit must be > 0"):
|
||||
GlobalRateLimiter(rate_limit=0, rate_window=60)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_init_rate_window_zero_raises(self):
|
||||
"""rate_window <= 0 raises ValueError."""
|
||||
GlobalRateLimiter.reset_instance()
|
||||
with pytest.raises(ValueError, match="rate_window must be > 0"):
|
||||
GlobalRateLimiter(rate_limit=10, rate_window=0)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_retry_exhaust_retries_raises(self):
|
||||
"""When all 429 retries exhausted, last exception is raised."""
|
||||
import openai
|
||||
from httpx import Response, Request
|
||||
|
||||
GlobalRateLimiter.reset_instance()
|
||||
limiter = GlobalRateLimiter.get_instance(rate_limit=100, rate_window=60)
|
||||
|
||||
def make_429():
|
||||
return openai.RateLimitError(
|
||||
"rate limited",
|
||||
response=Response(429, request=Request("POST", "http://x")),
|
||||
body={},
|
||||
)
|
||||
|
||||
async def fail():
|
||||
raise make_429()
|
||||
|
||||
with pytest.raises(openai.RateLimitError):
|
||||
await limiter.execute_with_retry(
|
||||
fail, max_retries=2, base_delay=0.01, max_delay=0.1, jitter=0
|
||||
)
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_execute_with_retry_succeeds_on_retry(self):
|
||||
"""429 then success returns result."""
|
||||
import openai
|
||||
from httpx import Response, Request
|
||||
|
||||
GlobalRateLimiter.reset_instance()
|
||||
limiter = GlobalRateLimiter.get_instance(rate_limit=100, rate_window=60)
|
||||
|
||||
def make_429():
|
||||
return openai.RateLimitError(
|
||||
"rate limited",
|
||||
response=Response(429, request=Request("POST", "http://x")),
|
||||
body={},
|
||||
)
|
||||
|
||||
call_count = 0
|
||||
|
||||
async def fail_then_ok():
|
||||
nonlocal call_count
|
||||
call_count += 1
|
||||
if call_count == 1:
|
||||
raise make_429()
|
||||
return "ok"
|
||||
|
||||
result = await limiter.execute_with_retry(
|
||||
fail_then_ok, max_retries=2, base_delay=0.01, max_delay=0.1, jitter=0
|
||||
)
|
||||
assert result == "ok"
|
||||
assert call_count == 2
|
||||
|
||||
@@ -97,6 +97,20 @@ def test_count_tokens_endpoint(client):
|
||||
assert response.json()["input_tokens"] == 5
|
||||
|
||||
|
||||
def test_count_tokens_error_returns_500(client):
|
||||
"""When get_token_count raises, count_tokens returns 500."""
|
||||
payload = {
|
||||
"model": "claude-3-sonnet",
|
||||
"messages": [{"role": "user", "content": "hello"}],
|
||||
}
|
||||
|
||||
with patch("api.routes.get_token_count", side_effect=RuntimeError("token error")):
|
||||
response = client.post("/v1/messages/count_tokens", json=payload)
|
||||
|
||||
assert response.status_code == 500
|
||||
assert "token error" in response.json()["detail"]
|
||||
|
||||
|
||||
def test_stop_cli_with_handler(client):
|
||||
mock_handler = MagicMock()
|
||||
# Mock the async method to return a completed future or just mock it since TestClient
|
||||
|
||||
Reference in New Issue
Block a user