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:
Quentin 2026-02-27 17:31:58 +01:00 committed by GitHub
parent a560a47ce8
commit 5d2e01a6d7
No known key found for this signature in database
GPG key ID: B5690EEEBB952194
139 changed files with 7152 additions and 1457 deletions

View file

@ -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"

View file

@ -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