Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Val <102326092+vdeva@users.noreply.github.com> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
1270 lines
46 KiB
Python
1270 lines
46 KiB
Python
from __future__ import annotations
|
|
|
|
from datetime import UTC, datetime, timedelta
|
|
import json
|
|
import os
|
|
from pathlib import Path
|
|
from unittest.mock import AsyncMock, MagicMock, patch
|
|
|
|
import pytest
|
|
|
|
from tests.conftest import build_test_vibe_config
|
|
from vibe.core.agents.models import AgentProfile, AgentSafety
|
|
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
|
from vibe.core.experiments.models import EvalResponse
|
|
from vibe.core.loop import ScheduledLoop
|
|
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 build_test_vibe_config(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 combined git command
|
|
git_mock = MagicMock()
|
|
git_mock.returncode = 0
|
|
git_mock.stdout = "abc123\nmain\n"
|
|
|
|
mock_subprocess.return_value = git_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())
|
|
assert metadata.title is None
|
|
assert metadata.title_source == "auto"
|
|
|
|
@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 combined git command 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 TestSessionLoggerTitleManagement:
|
|
def test_set_title_marks_live_session_title_as_manual(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
|
|
logger.set_title("Manual title")
|
|
|
|
assert logger.session_metadata is not None
|
|
assert logger.session_metadata.title == "Manual title"
|
|
assert logger.session_metadata.title_source == "manual"
|
|
|
|
def test_set_title_none_returns_live_session_to_auto_mode(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
logger.set_title("Manual title")
|
|
|
|
logger.set_title(None)
|
|
|
|
assert logger.session_metadata is not None
|
|
assert logger.session_metadata.title is None
|
|
assert logger.session_metadata.title_source == "auto"
|
|
|
|
def test_set_title_rejects_empty_title(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
|
|
with pytest.raises(ValueError, match="Session title cannot be empty."):
|
|
logger.set_title(" ")
|
|
|
|
def test_set_title_preserves_live_session_end_time(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
assert logger.session_metadata is not None
|
|
logger.session_metadata.end_time = "2026-01-01T10:00:00+00:00"
|
|
|
|
logger.set_title("Manual title")
|
|
|
|
assert logger.session_metadata.end_time == "2026-01-01T10:00:00+00:00"
|
|
|
|
def test_set_initial_auto_title_applies_when_no_title_set(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
|
|
applied = logger.set_initial_auto_title("Pretty title")
|
|
|
|
assert applied is True
|
|
assert logger.session_metadata is not None
|
|
assert logger.session_metadata.title == "Pretty title"
|
|
assert logger.session_metadata.title_source == "auto"
|
|
|
|
def test_set_initial_auto_title_noop_when_title_already_set(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
logger.set_title("Manual title")
|
|
|
|
applied = logger.set_initial_auto_title("Pretty title")
|
|
|
|
assert applied is False
|
|
assert logger.session_metadata is not None
|
|
assert logger.session_metadata.title == "Manual title"
|
|
assert logger.session_metadata.title_source == "manual"
|
|
|
|
def test_set_initial_auto_title_noop_when_prior_auto_title_set(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
logger.set_initial_auto_title("First title")
|
|
|
|
applied = logger.set_initial_auto_title("Second title")
|
|
|
|
assert applied is False
|
|
assert logger.session_metadata is not None
|
|
assert logger.session_metadata.title == "First title"
|
|
|
|
def test_set_initial_auto_title_rejects_blank(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
|
|
applied = logger.set_initial_auto_title(" ")
|
|
|
|
assert applied is False
|
|
assert logger.session_metadata is not None
|
|
assert logger.session_metadata.title is None
|
|
|
|
def test_needs_initial_auto_title_true_when_no_title(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
|
|
assert logger.needs_initial_auto_title() is True
|
|
|
|
def test_needs_initial_auto_title_false_after_set_initial_auto_title(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
logger.set_initial_auto_title("Pretty title")
|
|
|
|
assert logger.needs_initial_auto_title() is False
|
|
|
|
def test_needs_initial_auto_title_false_after_manual_set_title(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-123")
|
|
logger.set_title("Manual title")
|
|
|
|
assert logger.needs_initial_auto_title() is False
|
|
|
|
def test_needs_initial_auto_title_false_when_disabled(
|
|
self, disabled_session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(disabled_session_config, "test-session-123")
|
|
|
|
assert logger.needs_initial_auto_title() is False
|
|
|
|
|
|
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=build_test_vibe_config(
|
|
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
|
|
)
|
|
|
|
await logger.save_interaction(
|
|
messages=messages,
|
|
stats=stats,
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
# Verify behavior via file system
|
|
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()
|
|
|
|
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 metadata["title_source"] == "auto"
|
|
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
|
|
)
|
|
|
|
await logger.save_interaction(
|
|
messages=messages,
|
|
stats=stats,
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
assert logger.session_dir is not None
|
|
metadata_file = logger.session_dir / "meta.json"
|
|
assert metadata_file.exists()
|
|
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"
|
|
assert messages_file.exists()
|
|
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
|
|
)
|
|
|
|
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 behavior via file system: metadata was updated
|
|
assert logger.session_dir is not None
|
|
metadata_file = logger.session_dir / "meta.json"
|
|
assert metadata_file.exists()
|
|
with open(metadata_file) as f:
|
|
metadata = json.load(f)
|
|
assert metadata["total_messages"] == 4
|
|
assert metadata["stats"]["steps"] == updated_stats.steps
|
|
|
|
messages_file = logger.session_dir / "messages.jsonl"
|
|
assert messages_file.exists()
|
|
with open(messages_file) as f:
|
|
lines = f.readlines()
|
|
assert len(lines) == 4
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save_interaction_no_new_messages_is_noop(
|
|
self,
|
|
session_config: SessionLoggingConfig,
|
|
mock_vibe_config: VibeConfig,
|
|
mock_tool_manager: ToolManager,
|
|
mock_agent_profile: AgentProfile,
|
|
) -> None:
|
|
"""Test that save_interaction does nothing when there are no new 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
|
|
)
|
|
|
|
await logger.save_interaction(
|
|
messages=messages,
|
|
stats=stats,
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
assert logger.session_dir is not None
|
|
metadata_file = logger.session_dir / "meta.json"
|
|
messages_file = logger.session_dir / "messages.jsonl"
|
|
|
|
with open(metadata_file) as f:
|
|
meta_before = json.load(f)
|
|
with open(messages_file) as f:
|
|
lines_before = f.readlines()
|
|
|
|
# Call again with same messages: no new messages, should be no-op
|
|
await logger.save_interaction(
|
|
messages=messages,
|
|
stats=stats,
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
with open(metadata_file) as f:
|
|
meta_after = json.load(f)
|
|
with open(messages_file) as f:
|
|
lines_after = f.readlines()
|
|
|
|
assert len(lines_after) == len(lines_before) == 2
|
|
assert lines_after == lines_before
|
|
assert meta_after["total_messages"] == meta_before["total_messages"] == 2
|
|
assert meta_after == meta_before
|
|
|
|
@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
|
|
)
|
|
|
|
await logger.save_interaction(
|
|
messages=messages,
|
|
stats=stats,
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
# Verify behavior via file system
|
|
assert logger.session_dir is not None
|
|
metadata_file = logger.session_dir / "meta.json"
|
|
assert metadata_file.exists()
|
|
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"
|
|
assert metadata["title_source"] == "auto"
|
|
|
|
messages_file = logger.session_dir / "messages.jsonl"
|
|
assert messages_file.exists()
|
|
with open(messages_file) as f:
|
|
assert len(f.readlines()) == 1
|
|
|
|
@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
|
|
)
|
|
|
|
await logger.save_interaction(
|
|
messages=messages,
|
|
stats=stats,
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
# Verify behavior via file system
|
|
assert logger.session_dir is not None
|
|
metadata_file = logger.session_dir / "meta.json"
|
|
assert metadata_file.exists()
|
|
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
|
|
assert metadata["title_source"] == "auto"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save_interaction_preserves_preset_auto_title(
|
|
self,
|
|
session_config: SessionLoggingConfig,
|
|
mock_vibe_config: VibeConfig,
|
|
mock_tool_manager: ToolManager,
|
|
mock_agent_profile: AgentProfile,
|
|
) -> None:
|
|
session_id = "test-session-123"
|
|
logger = SessionLogger(session_config, session_id)
|
|
assert logger.session_metadata is not None
|
|
|
|
logger.set_initial_auto_title("Pretty @foo.py title")
|
|
|
|
messages = [
|
|
LLMMessage(role=Role.system, content="System prompt"),
|
|
LLMMessage(
|
|
role=Role.user, content="path: file:///abs/foo.py\ncontent: ..."
|
|
),
|
|
LLMMessage(role=Role.assistant, content="Hi there!"),
|
|
]
|
|
stats = AgentStats(
|
|
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
|
)
|
|
|
|
await logger.save_interaction(
|
|
messages=messages,
|
|
stats=stats,
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
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["title"] == "Pretty @foo.py title"
|
|
assert metadata["title_source"] == "auto"
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_save_interaction_preserves_manual_title(
|
|
self,
|
|
session_config: SessionLoggingConfig,
|
|
mock_vibe_config: VibeConfig,
|
|
mock_tool_manager: ToolManager,
|
|
mock_agent_profile: AgentProfile,
|
|
) -> None:
|
|
session_id = "test-session-123"
|
|
logger = SessionLogger(session_config, session_id)
|
|
assert logger.session_metadata is not None
|
|
|
|
logger.set_title("Manual title")
|
|
|
|
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=messages,
|
|
stats=stats,
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
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["title"] == "Manual title"
|
|
assert metadata["title_source"] == "manual"
|
|
|
|
messages_file = logger.session_dir / "messages.jsonl"
|
|
assert messages_file.exists()
|
|
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:
|
|
"""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()
|
|
|
|
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
|
|
|
|
|
|
class TestPersistLoops:
|
|
@pytest.mark.asyncio
|
|
async def test_writes_into_existing_metadata(
|
|
self,
|
|
session_config: SessionLoggingConfig,
|
|
mock_vibe_config: VibeConfig,
|
|
mock_tool_manager: ToolManager,
|
|
mock_agent_profile: AgentProfile,
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "test-session-loops")
|
|
await logger.save_interaction(
|
|
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),
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
assert logger.session_metadata is not None
|
|
logger.session_metadata.loops = [
|
|
ScheduledLoop(
|
|
id="aabbccdd",
|
|
interval_seconds=30,
|
|
prompt="ping",
|
|
next_fire_at=12345.0,
|
|
created_at=12000.0,
|
|
)
|
|
]
|
|
await logger.persist_loops()
|
|
|
|
assert logger.session_dir is not None
|
|
with open(logger.session_dir / "meta.json") as f:
|
|
metadata = json.load(f)
|
|
|
|
assert metadata["session_id"] == "test-session-loops"
|
|
assert metadata["total_messages"] == 2
|
|
assert metadata["loops"] == [
|
|
{
|
|
"id": "aabbccdd",
|
|
"interval_seconds": 30,
|
|
"prompt": "ping",
|
|
"next_fire_at": 12345.0,
|
|
"created_at": 12000.0,
|
|
}
|
|
]
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_noop_when_metadata_file_missing(
|
|
self, session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "no-meta-session")
|
|
assert logger.session_dir is not None
|
|
# No save_interaction was called -> meta.json does not exist
|
|
assert not (logger.session_dir / "meta.json").exists()
|
|
|
|
assert logger.session_metadata is not None
|
|
logger.session_metadata.loops = [
|
|
ScheduledLoop(
|
|
id="aabbccdd",
|
|
interval_seconds=30,
|
|
prompt="ping",
|
|
next_fire_at=1.0,
|
|
created_at=0.0,
|
|
)
|
|
]
|
|
await logger.persist_loops()
|
|
|
|
assert not (logger.session_dir / "meta.json").exists()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_noop_when_logging_disabled(
|
|
self, disabled_session_config: SessionLoggingConfig
|
|
) -> None:
|
|
logger = SessionLogger(disabled_session_config, "ignored")
|
|
# Should not raise even though session_metadata is None
|
|
await logger.persist_loops()
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_subsequent_save_interaction_preserves_loops(
|
|
self,
|
|
session_config: SessionLoggingConfig,
|
|
mock_vibe_config: VibeConfig,
|
|
mock_tool_manager: ToolManager,
|
|
mock_agent_profile: AgentProfile,
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "loops-vs-save")
|
|
messages = [
|
|
LLMMessage(role=Role.system, content="System prompt"),
|
|
LLMMessage(role=Role.user, content="Hello"),
|
|
LLMMessage(role=Role.assistant, content="Hi there!"),
|
|
]
|
|
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,
|
|
)
|
|
|
|
assert logger.session_metadata is not None
|
|
logger.session_metadata.loops = [
|
|
ScheduledLoop(
|
|
id="aabbccdd",
|
|
interval_seconds=30,
|
|
prompt="ping",
|
|
next_fire_at=12345.0,
|
|
created_at=12000.0,
|
|
)
|
|
]
|
|
await logger.persist_loops()
|
|
|
|
# A subsequent save (e.g. user sends another message) must not
|
|
# overwrite the on-disk loops with the stale in-memory value.
|
|
more_messages = [*messages, LLMMessage(role=Role.user, content="Again")]
|
|
await logger.save_interaction(
|
|
messages=more_messages,
|
|
stats=AgentStats(steps=2),
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
assert logger.session_dir is not None
|
|
with open(logger.session_dir / "meta.json") as f:
|
|
metadata = json.load(f)
|
|
assert len(metadata["loops"]) == 1
|
|
assert metadata["loops"][0]["id"] == "aabbccdd"
|
|
|
|
|
|
class TestPersistExperiments:
|
|
@pytest.fixture
|
|
def sample_response(self) -> EvalResponse:
|
|
return EvalResponse.model_validate({
|
|
"features": {
|
|
"vibe_code_cli_test_ab": {
|
|
"defaultValue": "cli",
|
|
"rules": [
|
|
{
|
|
"force": "cli_v2",
|
|
"tracks": [
|
|
{
|
|
"experiment": {"key": "vibe_code_cli_test_ab"},
|
|
"result": {
|
|
"key": "1",
|
|
"variationId": 1,
|
|
"inExperiment": True,
|
|
},
|
|
}
|
|
],
|
|
}
|
|
],
|
|
}
|
|
}
|
|
})
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_writes_field_into_existing_metadata(
|
|
self,
|
|
session_config: SessionLoggingConfig,
|
|
mock_vibe_config: VibeConfig,
|
|
mock_tool_manager: ToolManager,
|
|
mock_agent_profile: AgentProfile,
|
|
sample_response: EvalResponse,
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "exp-session")
|
|
await logger.save_interaction(
|
|
messages=[
|
|
LLMMessage(role=Role.system, content="System prompt"),
|
|
LLMMessage(role=Role.user, content="Hello"),
|
|
],
|
|
stats=AgentStats(steps=1),
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
await logger.persist_experiments(sample_response)
|
|
|
|
assert logger.session_dir is not None
|
|
with open(logger.session_dir / "meta.json") as f:
|
|
metadata = json.load(f)
|
|
assert "experiments" in metadata
|
|
assert (
|
|
metadata["experiments"]["features"]["vibe_code_cli_test_ab"]["defaultValue"]
|
|
== "cli"
|
|
)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_persists_none_as_null(
|
|
self,
|
|
session_config: SessionLoggingConfig,
|
|
mock_vibe_config: VibeConfig,
|
|
mock_tool_manager: ToolManager,
|
|
mock_agent_profile: AgentProfile,
|
|
) -> None:
|
|
logger = SessionLogger(session_config, "exp-none")
|
|
await logger.save_interaction(
|
|
messages=[
|
|
LLMMessage(role=Role.system, content="x"),
|
|
LLMMessage(role=Role.user, content="y"),
|
|
],
|
|
stats=AgentStats(steps=1),
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
await logger.persist_experiments(None)
|
|
|
|
assert logger.session_dir is not None
|
|
with open(logger.session_dir / "meta.json") as f:
|
|
metadata = json.load(f)
|
|
assert metadata.get("experiments") is None
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_does_not_create_metadata_file_when_missing(
|
|
self, session_config: SessionLoggingConfig, sample_response: EvalResponse
|
|
) -> None:
|
|
# Sessions without any message must not be persisted at all —
|
|
# persist_experiments updates only in-memory state when meta.json is
|
|
# absent, and lets the eventual save_interaction write it.
|
|
logger = SessionLogger(session_config, "fresh-session")
|
|
assert logger.session_dir is not None
|
|
assert not (logger.session_dir / "meta.json").exists()
|
|
|
|
await logger.persist_experiments(sample_response)
|
|
|
|
assert not (logger.session_dir / "meta.json").exists()
|
|
assert logger.session_metadata is not None
|
|
assert logger.session_metadata.experiments == sample_response
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_noop_when_logging_disabled(
|
|
self,
|
|
disabled_session_config: SessionLoggingConfig,
|
|
sample_response: EvalResponse,
|
|
) -> None:
|
|
logger = SessionLogger(disabled_session_config, "ignored")
|
|
await logger.persist_experiments(sample_response)
|
|
|
|
@pytest.mark.asyncio
|
|
async def test_first_save_interaction_includes_in_memory_experiments(
|
|
self,
|
|
session_config: SessionLoggingConfig,
|
|
mock_vibe_config: VibeConfig,
|
|
mock_tool_manager: ToolManager,
|
|
mock_agent_profile: AgentProfile,
|
|
sample_response: EvalResponse,
|
|
) -> None:
|
|
# Real flow: persist_experiments at session start (no meta.json yet,
|
|
# in-memory only). The first save_interaction must succeed AND
|
|
# include the experiments snapshot in the eventual meta.json.
|
|
logger = SessionLogger(session_config, "first-save-after-experiments")
|
|
await logger.persist_experiments(sample_response)
|
|
|
|
assert logger.session_dir is not None
|
|
assert not (logger.session_dir / "meta.json").exists()
|
|
|
|
await logger.save_interaction(
|
|
messages=[
|
|
LLMMessage(role=Role.system, content="System prompt"),
|
|
LLMMessage(role=Role.user, content="Hello"),
|
|
LLMMessage(role=Role.assistant, content="Hi"),
|
|
],
|
|
stats=AgentStats(steps=1),
|
|
base_config=mock_vibe_config,
|
|
tool_manager=mock_tool_manager,
|
|
agent_profile=mock_agent_profile,
|
|
)
|
|
|
|
with open(logger.session_dir / "meta.json") as f:
|
|
metadata = json.load(f)
|
|
assert metadata["total_messages"] == 2
|
|
assert (
|
|
metadata["experiments"]["features"]["vibe_code_cli_test_ab"]["defaultValue"]
|
|
== "cli"
|
|
)
|