v2.3.0 (#429)
Co-authored-by: Carlo <carloantonio.patti@mistral.ai> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Clement Sirieix <clem.sirieix@gmail.com> Co-authored-by: Michel Thomazo <michel.thomazo@mistral.ai> Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Vincent Guilloux <vincent.guilloux@mistral.ai> Co-authored-by: Thomas Kenbeek <thomas.kenbeek@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
a560a47ce8
commit
5d2e01a6d7
139 changed files with 7152 additions and 1457 deletions
|
|
@ -822,3 +822,133 @@ class TestSessionLoaderListSessions:
|
|||
assert len(result) == 1
|
||||
assert result[0]["session_id"] == "notitle0"
|
||||
assert result[0]["title"] is None
|
||||
|
||||
|
||||
class TestSessionLoaderGetFirstUserMessage:
|
||||
"""Tests for SessionLoader.get_first_user_message method."""
|
||||
|
||||
def test_returns_first_user_message(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test that get_first_user_message returns the first user message."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="First user message"),
|
||||
LLMMessage(role=Role.assistant, content="First response"),
|
||||
LLMMessage(role=Role.user, content="Second user message"),
|
||||
LLMMessage(role=Role.assistant, content="Second response"),
|
||||
]
|
||||
create_test_session(session_dir, "test-sess", messages=messages)
|
||||
|
||||
result = SessionLoader.get_first_user_message("test-sess", session_config)
|
||||
|
||||
assert result == "First user message"
|
||||
|
||||
def test_returns_fallback_for_missing_session(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that get_first_user_message returns fallback when session not found."""
|
||||
result = SessionLoader.get_first_user_message("nonexistent", session_config)
|
||||
|
||||
assert result == "(session not found)"
|
||||
|
||||
def test_returns_no_user_messages_fallback(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test fallback when session has no user messages."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.assistant, content="Assistant only"),
|
||||
]
|
||||
create_test_session(session_dir, "no-user0", messages=messages)
|
||||
|
||||
result = SessionLoader.get_first_user_message("no-user0", session_config)
|
||||
|
||||
assert result == "(no user messages)"
|
||||
|
||||
def test_replaces_newlines_with_spaces(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test that newlines in messages are replaced with spaces."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="Line one\nLine two\nLine three"),
|
||||
]
|
||||
create_test_session(session_dir, "newline0", messages=messages)
|
||||
|
||||
result = SessionLoader.get_first_user_message("newline0", session_config)
|
||||
|
||||
assert "\n" not in result
|
||||
assert "Line one Line two Line three" == result
|
||||
|
||||
def test_handles_empty_user_message(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test handling of empty user message content."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content=""),
|
||||
]
|
||||
create_test_session(session_dir, "empty-ms", messages=messages)
|
||||
|
||||
result = SessionLoader.get_first_user_message("empty-ms", session_config)
|
||||
|
||||
assert result == "(no user messages)"
|
||||
|
||||
def test_handles_whitespace_only_message(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test handling of whitespace-only user message."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content=" \n\t "),
|
||||
]
|
||||
create_test_session(session_dir, "whitespc", messages=messages)
|
||||
|
||||
result = SessionLoader.get_first_user_message("whitespc", session_config)
|
||||
|
||||
assert result == "(empty message)"
|
||||
|
||||
def test_handles_invalid_session_as_not_found(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that invalid sessions (bad JSON) are treated as not found.
|
||||
|
||||
Note: Sessions with invalid JSON are filtered out by _is_valid_session
|
||||
during find_session_by_id, so they return 'not found' rather than
|
||||
'corrupted'. This is the expected behavior.
|
||||
"""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
# Create a session with invalid JSON - will fail validation
|
||||
session_folder = session_dir / "test_20230101_120000_corrupt0"
|
||||
session_folder.mkdir()
|
||||
(session_folder / "messages.jsonl").write_text("{invalid json}")
|
||||
(session_folder / "meta.json").write_text('{"session_id": "corrupt0"}')
|
||||
|
||||
result = SessionLoader.get_first_user_message("corrupt0", session_config)
|
||||
|
||||
# Invalid sessions are filtered by _is_valid_session, so not found
|
||||
assert result == "(session not found)"
|
||||
|
||||
def test_skips_non_user_messages(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test that only user messages are considered, not assistant/system."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="User question"),
|
||||
LLMMessage(role=Role.assistant, content="Assistant response"),
|
||||
]
|
||||
create_test_session(session_dir, "skip-non", messages=messages)
|
||||
|
||||
result = SessionLoader.get_first_user_message("skip-non", session_config)
|
||||
|
||||
# Should return "User question", not "Assistant response"
|
||||
assert result == "User question"
|
||||
|
|
|
|||
|
|
@ -1,10 +1,10 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
from datetime import UTC, datetime, timedelta
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
from unittest.mock import AsyncMock, MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
|
|
@ -502,6 +502,61 @@ class TestSessionLoggerSaveInteraction:
|
|||
with open(messages_file) as f:
|
||||
assert len(f.readlines()) == 2
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_interaction_throttles_tmp_cleanup(
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
mock_vibe_config: VibeConfig,
|
||||
mock_tool_manager: ToolManager,
|
||||
mock_agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
logger = SessionLogger(session_config, "test-session-123")
|
||||
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
|
||||
cleanup_spy = MagicMock()
|
||||
with (
|
||||
patch.object(
|
||||
SessionLogger, "persist_messages", new_callable=AsyncMock
|
||||
) as persist_messages_mock,
|
||||
patch.object(
|
||||
SessionLogger, "persist_metadata", new_callable=AsyncMock
|
||||
) as persist_metadata_mock,
|
||||
patch.object(logger, "cleanup_tmp_files", cleanup_spy),
|
||||
patch(
|
||||
"vibe.core.session.session_logger.utc_now",
|
||||
# a bit brittle, but required for the call-count choregraphy...
|
||||
side_effect=[
|
||||
datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
|
||||
datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
|
||||
datetime(2026, 1, 1, 10, 0, 1, tzinfo=UTC),
|
||||
datetime(2026, 1, 1, 10, 0, 1, tzinfo=UTC),
|
||||
],
|
||||
),
|
||||
):
|
||||
await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=AgentStats(steps=1),
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=AgentStats(steps=2),
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
assert persist_messages_mock.await_count == 2
|
||||
assert persist_metadata_mock.await_count == 2
|
||||
assert cleanup_spy.call_count == 1
|
||||
|
||||
|
||||
class TestSessionLoggerResetSession:
|
||||
def test_reset_session(self, session_config: SessionLoggingConfig) -> None:
|
||||
|
|
@ -701,3 +756,27 @@ class TestSessionLoggerCleanupTmpFiles:
|
|||
|
||||
assert old_tmp_file.exists()
|
||||
assert not another_old_tmp_file.exists()
|
||||
|
||||
def test_maybe_cleanup_tmp_files_throttles_calls(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
cleanup_spy = MagicMock()
|
||||
with (
|
||||
patch.object(logger, "cleanup_tmp_files", cleanup_spy),
|
||||
patch(
|
||||
"vibe.core.session.session_logger.utc_now",
|
||||
side_effect=[
|
||||
datetime(2026, 1, 1, 10, 0, 0, tzinfo=UTC),
|
||||
datetime(2026, 1, 1, 10, 0, 1, tzinfo=UTC),
|
||||
datetime(2026, 1, 1, 10, 0, 6, tzinfo=UTC),
|
||||
],
|
||||
),
|
||||
):
|
||||
logger.maybe_cleanup_tmp_files()
|
||||
logger.maybe_cleanup_tmp_files()
|
||||
logger.maybe_cleanup_tmp_files()
|
||||
|
||||
assert cleanup_spy.call_count == 2
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue