2.0.0
Co-Authored-By: Quentin Torroba <quentin.torroba@mistral.ai> 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: Clément Siriex <clement.sirieix@mistral.ai> Co-Authored-By: Kim-Adeline Miguel <kimadeline.miguel@mistral.ai> Co-Authored-By: Thaddee Tyl <thaddee.tyl@gmail.com> Co-Authored-By: David Brochart <david.brochart@gmail.com> Co-Authored-By: Joseph Guhlin <joseph.guhlin@gmail.com> Co-Authored-By: Thomas Kenbeek <thomaskenbeek@gmail.com> Co-Authored-By: Remenby31 <baptiste.cruvellier31@gmail.com>
This commit is contained in:
parent
79f215d91c
commit
d33db9fff8
217 changed files with 16911 additions and 4305 deletions
539
tests/session/test_session_loader.py
Normal file
539
tests/session/test_session_loader.py
Normal file
|
|
@ -0,0 +1,539 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime
|
||||
import json
|
||||
from pathlib import Path
|
||||
import time
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config import SessionLoggingConfig
|
||||
from vibe.core.session.session_loader import SessionLoader
|
||||
from vibe.core.types import LLMMessage, Role, ToolCall
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_session_dir(tmp_path: Path) -> Path:
|
||||
"""Create a temporary directory for session loader tests."""
|
||||
session_dir = tmp_path / "sessions"
|
||||
session_dir.mkdir()
|
||||
return session_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session_config(temp_session_dir: Path) -> SessionLoggingConfig:
|
||||
"""Create a session logging config for testing."""
|
||||
return SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="test", enabled=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def create_test_session():
|
||||
"""Helper fixture to create a test session with messages and metadata."""
|
||||
|
||||
def _create_test_session(
|
||||
session_dir: Path,
|
||||
session_id: str,
|
||||
messages: list[LLMMessage] | None = None,
|
||||
metadata: dict | None = None,
|
||||
) -> Path:
|
||||
"""Create a test session directory with messages and metadata files."""
|
||||
# Create session directory
|
||||
timestamp = datetime.now().strftime("%Y%m%d_%H%M%S")
|
||||
session_folder = session_dir / f"test_{timestamp}_{session_id[:8]}"
|
||||
session_folder.mkdir(exist_ok=True)
|
||||
|
||||
# Create messages file
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
if messages is None:
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
|
||||
with messages_file.open("w", encoding="utf-8") as f:
|
||||
for message in messages:
|
||||
f.write(
|
||||
json.dumps(
|
||||
message.model_dump(exclude_none=True), ensure_ascii=False
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
# Create metadata file
|
||||
metadata_file = session_folder / "meta.json"
|
||||
if metadata is None:
|
||||
metadata = {
|
||||
"session_id": session_id,
|
||||
"start_time": "2023-01-01T12:00:00",
|
||||
"end_time": "2023-01-01T12:05:00",
|
||||
"total_messages": 2,
|
||||
"stats": {
|
||||
"steps": 1,
|
||||
"session_prompt_tokens": 10,
|
||||
"session_completion_tokens": 20,
|
||||
},
|
||||
"system_prompt": {"content": "System prompt", "role": "system"},
|
||||
}
|
||||
|
||||
with metadata_file.open("w", encoding="utf-8") as f:
|
||||
json.dump(metadata, f, indent=2)
|
||||
|
||||
return session_folder
|
||||
|
||||
return _create_test_session
|
||||
|
||||
|
||||
class TestSessionLoaderFindLatestSession:
|
||||
def test_find_latest_session_no_sessions(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test finding latest session when no sessions exist."""
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is None
|
||||
|
||||
def test_find_latest_session_single_session(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding latest session with a single session."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session = create_test_session(session_dir, "session-123")
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
assert result.exists()
|
||||
assert result == session
|
||||
|
||||
def test_find_latest_session_multiple_sessions(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding latest session with multiple sessions."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
create_test_session(session_dir, "session-123")
|
||||
time.sleep(0.01)
|
||||
create_test_session(session_dir, "session-456")
|
||||
time.sleep(0.01)
|
||||
latest = create_test_session(session_dir, "session-789")
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
assert result.exists()
|
||||
assert result == latest
|
||||
|
||||
def test_find_latest_session_nonexistent_save_dir(self) -> None:
|
||||
"""Test finding latest session when save directory doesn't exist."""
|
||||
# Modify config to point to non-existent directory
|
||||
bad_config = SessionLoggingConfig(
|
||||
save_dir="/nonexistent/path", session_prefix="test", enabled=True
|
||||
)
|
||||
|
||||
result = SessionLoader.find_latest_session(bad_config)
|
||||
assert result is None
|
||||
|
||||
def test_find_latest_session_with_invalid_sessions(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test finding latest session when only invalid sessions exist."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
invalid_session1 = session_dir / "test_20230101_120000_invalid1"
|
||||
invalid_session1.mkdir()
|
||||
(invalid_session1 / "messages.jsonl").write_text("[]") # Missing meta.json
|
||||
|
||||
invalid_session2 = session_dir / "test_20230101_120001_invalid2"
|
||||
invalid_session2.mkdir()
|
||||
(invalid_session2 / "meta.json").write_text('{"session_id": "invalid"}')
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is None # Should return None when no valid sessions exist
|
||||
|
||||
def test_find_latest_session_with_mixed_valid_invalid(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding latest session when both valid and invalid sessions exist."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
invalid_session = session_dir / "test_20230101_120000_invalid"
|
||||
invalid_session.mkdir()
|
||||
(invalid_session / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "test"}\n'
|
||||
)
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
valid_session = create_test_session(session_dir, "test_20230101_120001_valid")
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
newest_invalid = session_dir / "test_20230101_120002_newest"
|
||||
newest_invalid.mkdir()
|
||||
(newest_invalid / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "test"}\n'
|
||||
)
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
assert result == valid_session
|
||||
|
||||
def test_find_latest_session_with_invalid_json(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding latest session when sessions have invalid JSON."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
invalid_meta_session = session_dir / "test_20230101_120000_invalid_meta"
|
||||
invalid_meta_session.mkdir()
|
||||
(invalid_meta_session / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "test"}\n'
|
||||
)
|
||||
(invalid_meta_session / "meta.json").write_text("{invalid json}")
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
invalid_msg_session = session_dir / "test_20230101_120001_invalid_msg"
|
||||
invalid_msg_session.mkdir()
|
||||
(invalid_msg_session / "messages.jsonl").write_text("{invalid json}")
|
||||
(invalid_msg_session / "meta.json").write_text('{"session_id": "invalid"}')
|
||||
|
||||
time.sleep(0.01)
|
||||
|
||||
valid_session = create_test_session(session_dir, "test_20230101_120002_valid")
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
assert result == valid_session
|
||||
|
||||
|
||||
class TestSessionLoaderFindSessionById:
|
||||
def test_find_session_by_id_exact_match(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding session by exact ID match."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = create_test_session(session_dir, "test-session-123")
|
||||
|
||||
# Test with full UUID format
|
||||
result = SessionLoader.find_session_by_id("test-session-123", session_config)
|
||||
assert result is not None
|
||||
assert result == session_folder
|
||||
|
||||
def test_find_session_by_id_short_uuid(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding session by short UUID."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = create_test_session(
|
||||
session_dir, "abc12345-6789-0123-4567-89abcdef0123"
|
||||
)
|
||||
|
||||
# Test with short UUID
|
||||
result = SessionLoader.find_session_by_id("abc12345", session_config)
|
||||
assert result is not None
|
||||
assert result == session_folder
|
||||
|
||||
def test_find_session_by_id_partial_match(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding session by partial ID match"""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = create_test_session(session_dir, "abc12345678")
|
||||
|
||||
# Test with partial match
|
||||
result = SessionLoader.find_session_by_id("abc12345", session_config)
|
||||
assert result is not None
|
||||
assert result == session_folder
|
||||
|
||||
def test_find_session_by_id_multiple_matches(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding session when multiple sessions match (should return most recent)."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
# Create first session
|
||||
create_test_session(session_dir, "abcd1234")
|
||||
|
||||
# Sleep to ensure different modification times
|
||||
time.sleep(0.01)
|
||||
|
||||
# Create second session with similar ID prefix
|
||||
session_2 = create_test_session(session_dir, "abcd1234")
|
||||
|
||||
result = SessionLoader.find_session_by_id("abcd1234", session_config)
|
||||
assert result is not None
|
||||
assert result == session_2
|
||||
|
||||
def test_find_session_by_id_no_match(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding session by ID when no match exists."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
create_test_session(session_dir, "test-session-123")
|
||||
|
||||
result = SessionLoader.find_session_by_id("nonexistent", session_config)
|
||||
assert result is None
|
||||
|
||||
def test_find_session_by_id_nonexistent_save_dir(self) -> None:
|
||||
"""Test finding session by ID when save directory doesn't exist."""
|
||||
bad_config = SessionLoggingConfig(
|
||||
save_dir="/nonexistent/path", session_prefix="test", enabled=True
|
||||
)
|
||||
|
||||
result = SessionLoader.find_session_by_id("test-session", bad_config)
|
||||
assert result is None
|
||||
|
||||
|
||||
class TestSessionLoaderLoadSession:
|
||||
def test_load_session_success(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test successfully loading a session."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = create_test_session(session_dir, "test-session-123")
|
||||
|
||||
messages, metadata = SessionLoader.load_session(session_folder)
|
||||
|
||||
# Verify messages
|
||||
assert len(messages) == 2
|
||||
assert messages[0].role == "user"
|
||||
assert messages[0].content == "Hello"
|
||||
assert messages[1].role == "assistant"
|
||||
assert messages[1].content == "Hi there!"
|
||||
|
||||
# Verify metadata
|
||||
assert metadata["session_id"] == "test-session-123"
|
||||
assert metadata["total_messages"] == 2
|
||||
assert "stats" in metadata
|
||||
assert "system_prompt" in metadata
|
||||
|
||||
def test_load_session_empty_messages(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test loading session with empty messages file."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = session_dir / "test_20230101_120000_test123"
|
||||
session_folder.mkdir()
|
||||
|
||||
# Create empty messages file
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
messages_file.write_text("")
|
||||
|
||||
# Create metadata file
|
||||
metadata_file = session_folder / "meta.json"
|
||||
metadata_file.write_text('{"session_id": "test-session"}')
|
||||
|
||||
with pytest.raises(ValueError, match="Session messages file is empty"):
|
||||
SessionLoader.load_session(session_folder)
|
||||
|
||||
def test_load_session_invalid_json_messages(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test loading session with invalid JSON in messages file."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = session_dir / "test_20230101_120000_test123"
|
||||
session_folder.mkdir()
|
||||
|
||||
# Create messages file with invalid JSON
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
messages_file.write_text("{invalid json}")
|
||||
|
||||
# Create metadata file
|
||||
metadata_file = session_folder / "meta.json"
|
||||
metadata_file.write_text('{"session_id": "test-session"}')
|
||||
|
||||
with pytest.raises(ValueError, match="Session messages contain invalid JSON"):
|
||||
SessionLoader.load_session(session_folder)
|
||||
|
||||
def test_load_session_invalid_json_metadata(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test loading session with invalid JSON in metadata file."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = session_dir / "test_20230101_120000_test123"
|
||||
session_folder.mkdir()
|
||||
|
||||
# Create valid messages file
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
messages_file.write_text('{"role": "user", "content": "Hello"}\n')
|
||||
|
||||
# Create metadata file with invalid JSON
|
||||
metadata_file = session_folder / "meta.json"
|
||||
metadata_file.write_text("{invalid json}")
|
||||
|
||||
with pytest.raises(ValueError, match="Session metadata contains invalid JSON"):
|
||||
SessionLoader.load_session(session_folder)
|
||||
|
||||
def test_load_session_no_metadata_file(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test loading session when metadata file doesn't exist."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = session_dir / "test_20230101_120000_test123"
|
||||
session_folder.mkdir()
|
||||
|
||||
# Create valid messages file using the same format as create_test_session
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
with messages_file.open("w", encoding="utf-8") as f:
|
||||
for message in messages:
|
||||
f.write(
|
||||
json.dumps(
|
||||
message.model_dump(exclude_none=True), ensure_ascii=False
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
loaded_messages, metadata = SessionLoader.load_session(session_folder)
|
||||
|
||||
assert len(loaded_messages) == 2
|
||||
assert loaded_messages[0].content == "Hello"
|
||||
assert loaded_messages[0].role == Role.user
|
||||
assert loaded_messages[1].content == "Hi there!"
|
||||
assert loaded_messages[1].role == Role.assistant
|
||||
|
||||
assert metadata == {}
|
||||
|
||||
def test_load_session_nonexistent_directory(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test loading session from non-existent directory."""
|
||||
nonexistent_dir = Path(session_config.save_dir) / "nonexistent"
|
||||
|
||||
with pytest.raises(ValueError, match="Error reading session messages"):
|
||||
SessionLoader.load_session(nonexistent_dir)
|
||||
|
||||
|
||||
class TestSessionLoaderEdgeCases:
|
||||
def test_find_latest_session_with_different_prefixes(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test finding latest session when sessions have different prefixes."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
# Create sessions with different prefixes
|
||||
other_session = session_dir / "other_20230101_120000_test123"
|
||||
other_session.mkdir()
|
||||
(other_session / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "test"}\n'
|
||||
)
|
||||
|
||||
test_session = session_dir / "test_20230101_120000_test456"
|
||||
test_session.mkdir()
|
||||
(test_session / "messages.jsonl").write_text(
|
||||
'{"role": "user", "content": "test"}\n'
|
||||
)
|
||||
(test_session / "meta.json").write_text('{"session_id": "test456"}')
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
assert result.name == "test_20230101_120000_test456"
|
||||
|
||||
def test_find_session_by_id_with_special_characters(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
"""Test finding session by ID containing special characters."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = create_test_session(
|
||||
session_dir, "test-session_with-special.chars"
|
||||
)
|
||||
|
||||
result = SessionLoader.find_session_by_id(
|
||||
"test-session_with-special.chars", session_config
|
||||
)
|
||||
assert result is not None
|
||||
assert result == session_folder
|
||||
|
||||
def test_load_session_with_complex_messages(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test loading session with complex message structures."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = session_dir / "test_20230101_120000_test123"
|
||||
session_folder.mkdir()
|
||||
|
||||
# Create messages with complex structure
|
||||
complex_messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(
|
||||
role=Role.user,
|
||||
content="Complex message",
|
||||
reasoning_content="Some reasoning",
|
||||
tool_calls=[ToolCall(id="call1", index=1, type="function")],
|
||||
),
|
||||
LLMMessage(
|
||||
role=Role.assistant,
|
||||
content="Response",
|
||||
tool_calls=[ToolCall(id="call2", index=2, type="function")],
|
||||
),
|
||||
]
|
||||
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
with messages_file.open("w", encoding="utf-8") as f:
|
||||
for message in complex_messages:
|
||||
f.write(
|
||||
json.dumps(
|
||||
message.model_dump(exclude_none=True), ensure_ascii=False
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
# Create metadata file
|
||||
metadata_file = session_folder / "meta.json"
|
||||
metadata_file.write_text('{"session_id": "complex-session"}')
|
||||
|
||||
messages, _ = SessionLoader.load_session(session_folder)
|
||||
|
||||
# Verify complex messages are loaded correctly
|
||||
assert len(messages) == 2
|
||||
assert messages[0].role == Role.user
|
||||
assert messages[0].content == "Complex message"
|
||||
assert messages[0].reasoning_content == "Some reasoning"
|
||||
assert len(messages[0].tool_calls or []) == 1
|
||||
assert messages[1].role == Role.assistant
|
||||
assert len(messages[1].tool_calls or []) == 1
|
||||
assert messages[1].content == "Response"
|
||||
|
||||
def test_load_session_system_prompt_ignored_in_messages(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that system prompt is ignored when written in messages.jsonl."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = session_dir / "test_20230101_120000_test123"
|
||||
session_folder.mkdir()
|
||||
|
||||
messages_with_system = [
|
||||
LLMMessage(role=Role.system, content="System prompt from messages"),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
|
||||
messages_file = session_folder / "messages.jsonl"
|
||||
with messages_file.open("w", encoding="utf-8") as f:
|
||||
for message in messages_with_system:
|
||||
f.write(
|
||||
json.dumps(
|
||||
message.model_dump(exclude_none=True), ensure_ascii=False
|
||||
)
|
||||
+ "\n"
|
||||
)
|
||||
|
||||
metadata_file = session_folder / "meta.json"
|
||||
metadata_file.write_text(
|
||||
json.dumps({"session_id": "test-session", "total_messages": 3})
|
||||
)
|
||||
|
||||
messages, metadata = SessionLoader.load_session(session_folder)
|
||||
|
||||
# Verify that system prompt from messages.jsonl is ignored
|
||||
assert len(messages) == 2
|
||||
assert messages[0].role == Role.user
|
||||
assert messages[0].content == "Hello"
|
||||
assert messages[1].role == Role.assistant
|
||||
assert messages[1].content == "Hi there!"
|
||||
641
tests/session/test_session_logger.py
Normal file
641
tests/session/test_session_logger.py
Normal file
|
|
@ -0,0 +1,641 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from datetime import datetime, timedelta
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
from unittest.mock import MagicMock, patch
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.agents.models import AgentProfile, AgentSafety
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
from vibe.core.session.session_logger import SessionLogger
|
||||
from vibe.core.tools.manager import ToolManager
|
||||
from vibe.core.types import AgentStats, LLMMessage, Role, SessionMetadata
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_session_dir(tmp_path: Path) -> Path:
|
||||
"""Create a temporary directory for session logging tests."""
|
||||
session_dir = tmp_path / "sessions"
|
||||
session_dir.mkdir()
|
||||
return session_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session_config(temp_session_dir: Path) -> SessionLoggingConfig:
|
||||
"""Create a session logging config for testing."""
|
||||
return SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="test", enabled=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def disabled_session_config() -> SessionLoggingConfig:
|
||||
"""Create a disabled session logging config for testing."""
|
||||
return SessionLoggingConfig(
|
||||
save_dir="/tmp/test", session_prefix="test", enabled=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_agent_profile() -> AgentProfile:
|
||||
"""Create a mock agent profile for testing."""
|
||||
return AgentProfile(
|
||||
name="test-agent",
|
||||
display_name="Test Agent",
|
||||
description="A test agent",
|
||||
safety=AgentSafety.NEUTRAL,
|
||||
overrides={},
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_tool_manager() -> ToolManager:
|
||||
"""Create a mock tool manager for testing."""
|
||||
manager = MagicMock(spec=ToolManager)
|
||||
manager.available_tools = {}
|
||||
return manager
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def mock_vibe_config() -> VibeConfig:
|
||||
"""Create a mock vibe config for testing."""
|
||||
return VibeConfig(active_model="test-model", models=[], providers=[])
|
||||
|
||||
|
||||
class TestSessionLoggerInitialization:
|
||||
def test_enabled_session_logger_initialization(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that SessionLogger initializes correctly when enabled."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
assert logger.enabled is True
|
||||
assert logger.session_id == session_id
|
||||
assert logger.save_dir == Path(session_config.save_dir)
|
||||
assert logger.session_prefix == session_config.session_prefix
|
||||
assert logger.session_dir is not None
|
||||
assert logger.session_metadata is not None
|
||||
assert isinstance(logger.session_metadata, SessionMetadata)
|
||||
|
||||
# Check that session directory was created
|
||||
assert logger.session_dir is not None
|
||||
assert str(logger.session_dir).startswith(str(session_config.save_dir))
|
||||
|
||||
# Check session directory name format
|
||||
dir_name = logger.session_dir.name
|
||||
assert dir_name.startswith(f"{session_config.session_prefix}_")
|
||||
assert session_id[:8] in dir_name
|
||||
|
||||
def test_disabled_session_logger_initialization(
|
||||
self, disabled_session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that SessionLogger initializes correctly when disabled."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(disabled_session_config, session_id)
|
||||
|
||||
assert logger.enabled is False
|
||||
assert logger.session_id == "disabled"
|
||||
assert logger.save_dir is None
|
||||
assert logger.session_prefix is None
|
||||
assert logger.session_dir is None
|
||||
assert logger.session_metadata is None
|
||||
|
||||
|
||||
class TestSessionLoggerMetadata:
|
||||
@patch("vibe.core.session.session_logger.subprocess.run")
|
||||
@patch("vibe.core.session.session_logger.getpass.getuser")
|
||||
def test_session_metadata_initialization(
|
||||
self, mock_getuser, mock_subprocess, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that session metadata is correctly initialized."""
|
||||
# Mock git commands
|
||||
git_commit_mock = MagicMock()
|
||||
git_commit_mock.returncode = 0
|
||||
git_commit_mock.stdout = "abc123\n"
|
||||
|
||||
git_branch_mock = MagicMock()
|
||||
git_branch_mock.returncode = 0
|
||||
git_branch_mock.stdout = "main\n"
|
||||
|
||||
mock_subprocess.side_effect = [git_commit_mock, git_branch_mock]
|
||||
mock_getuser.return_value = "testuser"
|
||||
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
assert logger.session_metadata is not None
|
||||
metadata = logger.session_metadata
|
||||
|
||||
assert metadata.session_id == session_id
|
||||
assert metadata.start_time == logger.session_start_time
|
||||
assert metadata.end_time is None
|
||||
assert metadata.git_commit == "abc123"
|
||||
assert metadata.git_branch == "main"
|
||||
assert metadata.username == "testuser"
|
||||
assert "working_directory" in metadata.environment
|
||||
assert metadata.environment["working_directory"] == str(Path.cwd())
|
||||
|
||||
@patch("vibe.core.session.session_logger.subprocess.run")
|
||||
@patch("vibe.core.session.session_logger.getpass.getuser")
|
||||
def test_session_metadata_with_git_errors(
|
||||
self, mock_getuser, mock_subprocess, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that session metadata handles git command errors gracefully."""
|
||||
# Mock git commands to fail
|
||||
mock_subprocess.side_effect = FileNotFoundError("git not found")
|
||||
mock_getuser.return_value = "testuser"
|
||||
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
assert logger.session_metadata is not None
|
||||
metadata = logger.session_metadata
|
||||
|
||||
assert metadata.git_commit is None
|
||||
assert metadata.git_branch is None
|
||||
assert metadata.username == "testuser"
|
||||
|
||||
|
||||
class TestSessionLoggerSaveInteraction:
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_interaction_disabled(
|
||||
self, disabled_session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that save_interaction returns None when logging is disabled."""
|
||||
logger = SessionLogger(disabled_session_config, "test-session")
|
||||
|
||||
result = await logger.save_interaction(
|
||||
messages=[],
|
||||
stats=AgentStats(),
|
||||
base_config=VibeConfig(active_model="test", models=[], providers=[]),
|
||||
tool_manager=MagicMock(),
|
||||
agent_profile=AgentProfile(
|
||||
name="test",
|
||||
display_name="Test",
|
||||
description="Test agent",
|
||||
safety=AgentSafety.NEUTRAL,
|
||||
overrides={},
|
||||
),
|
||||
)
|
||||
|
||||
assert result is None
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_interaction_success(
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
mock_vibe_config: VibeConfig,
|
||||
mock_tool_manager: ToolManager,
|
||||
mock_agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
"""Test that save_interaction successfully saves session data."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
# Create test messages
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
|
||||
# Create test stats
|
||||
stats = AgentStats(
|
||||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
# Test that save_interaction returns a path when enabled
|
||||
result = await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert result is not None
|
||||
assert str(logger.session_dir) in result
|
||||
|
||||
# Verify that files were created
|
||||
assert logger.session_dir is not None
|
||||
messages_file = logger.session_dir / "messages.jsonl"
|
||||
metadata_file = logger.session_dir / "meta.json"
|
||||
|
||||
assert messages_file.exists()
|
||||
assert metadata_file.exists()
|
||||
|
||||
# Verify that metadata contains expected data
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
assert metadata["session_id"] == session_id
|
||||
assert metadata["total_messages"] == 2
|
||||
assert metadata["stats"]["steps"] == stats.steps
|
||||
assert "title" in metadata
|
||||
assert metadata["title"] == "Hello"
|
||||
assert "system_prompt" in metadata
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_interaction_system_prompt_in_metadata(
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
mock_vibe_config: VibeConfig,
|
||||
mock_tool_manager: ToolManager,
|
||||
mock_agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
"""Test that system prompt is saved in metadata and not in messages."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
|
||||
stats = AgentStats(
|
||||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
result = await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
assert logger.session_dir is not None
|
||||
|
||||
metadata_file = logger.session_dir / "meta.json"
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
assert "system_prompt" in metadata
|
||||
assert metadata["system_prompt"]["content"] == "System prompt"
|
||||
assert metadata["system_prompt"]["role"] == "system"
|
||||
|
||||
messages_file = logger.session_dir / "messages.jsonl"
|
||||
with open(messages_file) as f:
|
||||
lines = f.readlines()
|
||||
messages_data = [json.loads(line) for line in lines]
|
||||
|
||||
assert len(messages_data) == 2
|
||||
assert messages_data[0]["role"] == "user"
|
||||
assert messages_data[1]["role"] == "assistant"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_interaction_with_existing_messages(
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
mock_vibe_config: VibeConfig,
|
||||
mock_tool_manager: ToolManager,
|
||||
mock_agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
"""Test that save_interaction correctly handles existing messages."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
# First save - create initial session
|
||||
initial_messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content="Hello"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
|
||||
stats = AgentStats(
|
||||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
await logger.save_interaction(
|
||||
messages=initial_messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
# Second save - add more messages
|
||||
new_messages = [
|
||||
LLMMessage(role=Role.user, content="How are you?"),
|
||||
LLMMessage(role=Role.assistant, content="I'm fine, thanks!"),
|
||||
]
|
||||
|
||||
all_messages = initial_messages + new_messages
|
||||
updated_stats = AgentStats(
|
||||
steps=2, session_prompt_tokens=20, session_completion_tokens=40
|
||||
)
|
||||
|
||||
result = await logger.save_interaction(
|
||||
messages=all_messages,
|
||||
stats=updated_stats,
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
# Verify that the result is not None
|
||||
assert result is not None
|
||||
|
||||
# Verify that metadata was updated
|
||||
assert logger.session_dir is not None
|
||||
metadata_file = logger.session_dir / "meta.json"
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
assert metadata["total_messages"] == 4
|
||||
assert metadata["stats"]["steps"] == updated_stats.steps
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_interaction_no_user_messages(
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
mock_vibe_config: VibeConfig,
|
||||
mock_tool_manager: ToolManager,
|
||||
mock_agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
"""Test that save_interaction handles sessions with no user messages."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
# Create messages with no user messages (only system and assistant)
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.assistant, content="Hi there!"),
|
||||
]
|
||||
|
||||
stats = AgentStats(
|
||||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
result = await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert result is not None
|
||||
assert str(logger.session_dir) in result
|
||||
|
||||
# Verify that metadata contains expected data
|
||||
assert logger.session_dir is not None
|
||||
metadata_file = logger.session_dir / "meta.json"
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
assert metadata["session_id"] == session_id
|
||||
assert metadata["total_messages"] == 1
|
||||
assert metadata["stats"]["steps"] == stats.steps
|
||||
assert metadata["title"] == "Untitled session"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_save_interaction_long_user_message(
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
mock_vibe_config: VibeConfig,
|
||||
mock_tool_manager: ToolManager,
|
||||
mock_agent_profile: AgentProfile,
|
||||
) -> None:
|
||||
"""Test that save_interaction truncates long user messages for title."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
# Create a long user message (more than 50 characters)
|
||||
long_message = "This is a very long user message that exceeds fifty characters and should be truncated"
|
||||
messages = [
|
||||
LLMMessage(role=Role.system, content="System prompt"),
|
||||
LLMMessage(role=Role.user, content=long_message),
|
||||
LLMMessage(role=Role.assistant, content="Response"),
|
||||
]
|
||||
|
||||
stats = AgentStats(
|
||||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
result = await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
tool_manager=mock_tool_manager,
|
||||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
# Verify the result
|
||||
assert result is not None
|
||||
assert str(logger.session_dir) in result
|
||||
|
||||
# Verify that metadata contains expected data
|
||||
assert logger.session_dir is not None
|
||||
metadata_file = logger.session_dir / "meta.json"
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
assert metadata["session_id"] == session_id
|
||||
assert metadata["total_messages"] == 2
|
||||
assert metadata["stats"]["steps"] == stats.steps
|
||||
expected_title = long_message[:50] + "…"
|
||||
assert metadata["title"] == expected_title
|
||||
|
||||
|
||||
class TestSessionLoggerResetSession:
|
||||
def test_reset_session(self, session_config: SessionLoggingConfig) -> None:
|
||||
"""Test that reset_session correctly resets session information."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
# Store original session info
|
||||
original_session_id = logger.session_id
|
||||
original_metadata = logger.session_metadata
|
||||
|
||||
# Reset session
|
||||
new_session_id = "test-session-456"
|
||||
logger.reset_session(new_session_id)
|
||||
|
||||
# Verify session was reset
|
||||
assert logger.session_id == new_session_id
|
||||
assert logger.session_start_time != "N/A" # Should be a valid timestamp
|
||||
assert logger.session_metadata is not None
|
||||
assert logger.session_metadata.session_id == new_session_id
|
||||
|
||||
# Verify that metadata was recreated (different object)
|
||||
assert logger.session_metadata is not original_metadata
|
||||
|
||||
assert logger.session_id != original_session_id
|
||||
|
||||
def test_reset_session_disabled(
|
||||
self, disabled_session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that reset_session does nothing when logging is disabled."""
|
||||
logger = SessionLogger(disabled_session_config, "test-session")
|
||||
|
||||
# Reset session should not raise any errors
|
||||
logger.reset_session("new-session")
|
||||
|
||||
# Verify state is unchanged
|
||||
assert logger.enabled is False
|
||||
assert logger.session_id == "disabled"
|
||||
|
||||
|
||||
class TestSessionLoggerFileOperations:
|
||||
def test_save_folder(self, session_config: SessionLoggingConfig) -> None:
|
||||
"""Test that save_folder creates correct folder name."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
folder = logger.save_folder
|
||||
|
||||
assert folder.parent == Path(session_config.save_dir)
|
||||
assert folder.name.startswith(f"{session_config.session_prefix}_")
|
||||
assert session_id[:8] in folder.name
|
||||
|
||||
def test_metadata_filepath(self, session_config: SessionLoggingConfig) -> None:
|
||||
"""Test that metadata_filepath returns correct path."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
metadata_file = logger.metadata_filepath
|
||||
|
||||
assert logger.session_dir is not None
|
||||
assert metadata_file == logger.session_dir / "meta.json"
|
||||
|
||||
def test_messages_filepath(self, session_config: SessionLoggingConfig) -> None:
|
||||
"""Test that messages_filepath returns correct path."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
messages_file = logger.messages_filepath
|
||||
|
||||
assert logger.session_dir is not None
|
||||
assert messages_file == logger.session_dir / "messages.jsonl"
|
||||
|
||||
def test_disabled_file_operations_raise_errors(
|
||||
self, disabled_session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that file operations raise errors when logging is disabled."""
|
||||
logger = SessionLogger(disabled_session_config, "test-session")
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match="Cannot get session save folder when logging is disabled",
|
||||
):
|
||||
assert logger.save_folder is None
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match="Cannot get session metadata filepath when logging is disabled",
|
||||
):
|
||||
assert logger.metadata_filepath is None
|
||||
|
||||
with pytest.raises(
|
||||
RuntimeError,
|
||||
match="Cannot get session messages filepath when logging is disabled",
|
||||
):
|
||||
assert logger.messages_filepath is None
|
||||
|
||||
|
||||
def create_temp_file_ago(tmp_path: Path, filename: str, minutes_ago: int = 0) -> Path:
|
||||
"""Create a file with a modification time of `minutes_ago` minutes ago."""
|
||||
file = tmp_path / filename
|
||||
file.touch()
|
||||
old_time = datetime.now() - timedelta(minutes=minutes_ago)
|
||||
os.utime(file, (old_time.timestamp(), old_time.timestamp()))
|
||||
return file
|
||||
|
||||
|
||||
class TestSessionLoggerCleanupTmpFiles:
|
||||
def test_cleanup_tmp_files_disabled(
|
||||
self, disabled_session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that cleanup_tmp_files returns early when logging is disabled."""
|
||||
logger = SessionLogger(disabled_session_config, "test-session")
|
||||
|
||||
logger.cleanup_tmp_files()
|
||||
|
||||
def test_cleanup_tmp_files_no_tmp_files(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that cleanup_tmp_files handles no tmp files gracefully."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
logger.cleanup_tmp_files()
|
||||
|
||||
def test_cleanup_tmp_files_deletes_old_files(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that cleanup_tmp_files deletes tmp files older than 5 minutes."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
assert logger.session_dir is not None
|
||||
logger.session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
old_tmp_file = create_temp_file_ago(
|
||||
logger.session_dir, "session-123.json.tmp", 10
|
||||
)
|
||||
new_tmp_file = create_temp_file_ago(logger.session_dir, "session-123.json")
|
||||
|
||||
logger.cleanup_tmp_files()
|
||||
|
||||
assert not old_tmp_file.exists()
|
||||
assert new_tmp_file.exists()
|
||||
|
||||
def test_cleanup_tmp_files_recursive(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that cleanup_tmp_files works recursively in subdirectories."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
assert logger.session_dir is not None
|
||||
logger.session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
subdir_1 = logger.session_dir / "session-123"
|
||||
subdir_1.mkdir()
|
||||
|
||||
old_tmp_file = create_temp_file_ago(subdir_1, "meta.json.tmp", 10)
|
||||
new_tmp_file = create_temp_file_ago(subdir_1, "meta.json")
|
||||
|
||||
subdir_2 = logger.session_dir / "session-456"
|
||||
subdir_2.mkdir()
|
||||
|
||||
old_tmp_file_2 = create_temp_file_ago(subdir_2, "meta.json.tmp", 10)
|
||||
|
||||
logger.cleanup_tmp_files()
|
||||
|
||||
assert not old_tmp_file.exists()
|
||||
assert not old_tmp_file_2.exists()
|
||||
assert new_tmp_file.exists()
|
||||
|
||||
def test_cleanup_tmp_files_handles_exceptions(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that cleanup_tmp_files handles exceptions gracefully."""
|
||||
session_id = "test-session-123"
|
||||
logger = SessionLogger(session_config, session_id)
|
||||
|
||||
assert logger.session_dir is not None
|
||||
logger.session_dir.mkdir(parents=True, exist_ok=True)
|
||||
|
||||
old_tmp_file = create_temp_file_ago(logger.session_dir, "meta.json.tmp", 10)
|
||||
another_old_tmp_file = create_temp_file_ago(
|
||||
logger.session_dir, "meta-002.json.tmp", 10
|
||||
)
|
||||
|
||||
# Mock the unlink method to raise an exception for the first file
|
||||
original_unlink = Path.unlink
|
||||
|
||||
def mock_unlink(self):
|
||||
if str(self) == str(old_tmp_file):
|
||||
raise OSError("Mocked error")
|
||||
return original_unlink(self)
|
||||
|
||||
with patch.object(Path, "unlink", mock_unlink):
|
||||
logger.cleanup_tmp_files()
|
||||
|
||||
assert old_tmp_file.exists()
|
||||
assert not another_old_tmp_file.exists()
|
||||
177
tests/session/test_session_migration.py
Normal file
177
tests/session/test_session_migration.py
Normal file
|
|
@ -0,0 +1,177 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from vibe.core.config import SessionLoggingConfig
|
||||
from vibe.core.session.session_migration import migrate_sessions
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def temp_session_dir(tmp_path: Path) -> Path:
|
||||
session_dir = tmp_path / "sessions"
|
||||
session_dir.mkdir()
|
||||
return session_dir
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def session_config(temp_session_dir: Path) -> SessionLoggingConfig:
|
||||
return SessionLoggingConfig(
|
||||
save_dir=str(temp_session_dir), session_prefix="test", enabled=True
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def disabled_session_config() -> SessionLoggingConfig:
|
||||
return SessionLoggingConfig(
|
||||
save_dir="/tmp/test", session_prefix="test", enabled=False
|
||||
)
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
def old_session_data() -> dict:
|
||||
return {
|
||||
"metadata": {
|
||||
"session_id": "test-session-123",
|
||||
"start_time": "2023-01-01T00:00:00",
|
||||
"end_time": "2023-01-01T01:00:00",
|
||||
"git_commit": "abc123",
|
||||
"git_branch": "main",
|
||||
"username": "testuser",
|
||||
"environment": {"working_directory": "/test"},
|
||||
},
|
||||
"messages": [
|
||||
{"role": "system", "content": "System prompt"},
|
||||
{"role": "user", "content": "Hello"},
|
||||
{"role": "assistant", "content": "Hi there!"},
|
||||
],
|
||||
}
|
||||
|
||||
|
||||
class TestSessionMigration:
|
||||
@pytest.mark.asyncio
|
||||
async def test_migrate_sessions_disabled_config(
|
||||
self, disabled_session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that migration does nothing when config is disabled."""
|
||||
result = await migrate_sessions(disabled_session_config)
|
||||
assert result == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migrate_sessions_no_save_dir(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that migration handles missing save_dir gracefully."""
|
||||
config = SessionLoggingConfig(save_dir="", session_prefix="test", enabled=True)
|
||||
result = await migrate_sessions(config)
|
||||
assert result == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migrate_sessions_no_old_files(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that migration handles no old session files gracefully."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_dir.mkdir(exist_ok=True)
|
||||
|
||||
result = await migrate_sessions(session_config)
|
||||
assert result == 0
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migrate_sessions_successful_migration(
|
||||
self, session_config: SessionLoggingConfig, old_session_data: dict
|
||||
) -> None:
|
||||
"""Test successful migration of old session files."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
old_session_file = session_dir / "test_session-123.json"
|
||||
with open(old_session_file, "w") as f:
|
||||
json.dump(old_session_data, f)
|
||||
|
||||
result = await migrate_sessions(session_config)
|
||||
assert result == 1
|
||||
|
||||
assert not old_session_file.exists()
|
||||
session_subdir = session_dir / "test_session-123"
|
||||
assert session_subdir.is_dir()
|
||||
|
||||
metadata_file = session_subdir / "meta.json"
|
||||
assert metadata_file.is_file()
|
||||
|
||||
with open(metadata_file) as f:
|
||||
metadata = json.load(f)
|
||||
assert metadata == old_session_data["metadata"]
|
||||
|
||||
messages_file = session_subdir / "messages.jsonl"
|
||||
assert messages_file.exists()
|
||||
|
||||
with open(messages_file) as f:
|
||||
lines = f.readlines()
|
||||
assert len(lines) == len(old_session_data["messages"])
|
||||
|
||||
for i, line in enumerate(lines):
|
||||
message = json.loads(line.strip())
|
||||
assert message == old_session_data["messages"][i]
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migrate_sessions_multiple_files(
|
||||
self, session_config: SessionLoggingConfig, old_session_data: dict
|
||||
) -> None:
|
||||
"""Test migration of multiple old session files."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
session_files = []
|
||||
for i in range(3):
|
||||
session_file = session_dir / f"test_session-{i:03d}.json"
|
||||
session_files.append(session_file)
|
||||
|
||||
modified_data = old_session_data.copy()
|
||||
modified_data["metadata"]["session_id"] = f"test-session-{i}"
|
||||
|
||||
with open(session_file, "w") as f:
|
||||
json.dump(modified_data, f)
|
||||
|
||||
result = await migrate_sessions(session_config)
|
||||
assert result == 3
|
||||
|
||||
for session_file in session_files:
|
||||
assert not session_file.exists()
|
||||
|
||||
for i in range(3):
|
||||
session_subdir = session_dir / f"test_session-{i:03d}"
|
||||
assert session_subdir.exists()
|
||||
assert session_subdir.is_dir()
|
||||
|
||||
metadata_file = session_subdir / "meta.json"
|
||||
messages_file = session_subdir / "messages.jsonl"
|
||||
assert metadata_file.exists()
|
||||
assert messages_file.exists()
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_migrate_sessions_error_handling(
|
||||
self, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that migration handles errors gracefully and continues."""
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
valid_session_file = session_dir / "test_session-valid.json"
|
||||
valid_data = {
|
||||
"metadata": {"session_id": "valid-session"},
|
||||
"messages": [{"role": "user", "content": "Hello"}],
|
||||
}
|
||||
with open(valid_session_file, "w") as f:
|
||||
json.dump(valid_data, f)
|
||||
|
||||
invalid_session_file = session_dir / "test_session-invalid.json"
|
||||
with open(invalid_session_file, "w") as f:
|
||||
f.write("{invalid json}")
|
||||
|
||||
result = await migrate_sessions(session_config)
|
||||
assert result == 1
|
||||
|
||||
valid_session_subdir = session_dir / "test_session-valid"
|
||||
assert valid_session_subdir.exists()
|
||||
assert not valid_session_file.exists()
|
||||
assert invalid_session_file.exists()
|
||||
Loading…
Add table
Add a link
Reference in a new issue