v2.10.0 (#697)
Co-authored-by: Clément Drouin <clement.drouin@mistral.ai> Co-authored-by: Corentin André <corentin.andre@mistral.ai> Co-authored-by: Guillaume LE GOFF <guillaume.lgf@gmail.com> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: Peter Evers <pevers90@gmail.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: MichisGitIsKing <MichisGitIsKing@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
626f905186
commit
228f3c65a9
158 changed files with 7235 additions and 916 deletions
|
|
@ -87,13 +87,6 @@ class TestListRemoteResumeSessions:
|
|||
start_time=datetime(2026, 1, 1),
|
||||
end_time=None,
|
||||
)
|
||||
retrying = WorkflowExecutionWithoutResultResponse(
|
||||
workflow_name="vibe",
|
||||
execution_id="exec-retrying",
|
||||
status=WorkflowExecutionStatus.RETRYING_AFTER_ERROR,
|
||||
start_time=datetime(2026, 1, 1),
|
||||
end_time=None,
|
||||
)
|
||||
continued = WorkflowExecutionWithoutResultResponse(
|
||||
workflow_name="vibe",
|
||||
execution_id="exec-continued",
|
||||
|
|
@ -102,9 +95,7 @@ class TestListRemoteResumeSessions:
|
|||
end_time=None,
|
||||
)
|
||||
|
||||
mock_response = WorkflowExecutionListResponse(
|
||||
executions=[running, retrying, continued]
|
||||
)
|
||||
mock_response = WorkflowExecutionListResponse(executions=[running, continued])
|
||||
|
||||
config = MagicMock()
|
||||
config.vibe_code_enabled = True
|
||||
|
|
@ -121,10 +112,9 @@ class TestListRemoteResumeSessions:
|
|||
|
||||
result = await list_remote_resume_sessions(config)
|
||||
|
||||
assert len(result) == 3
|
||||
assert len(result) == 2
|
||||
session_ids = {s.session_id for s in result}
|
||||
assert "exec-running" in session_ids
|
||||
assert "exec-retrying" in session_ids
|
||||
assert "exec-continued" in session_ids
|
||||
assert all(s.source == "remote" for s in result)
|
||||
|
||||
|
|
@ -133,7 +123,6 @@ class TestListRemoteResumeSessions:
|
|||
page_size=50,
|
||||
status=[
|
||||
WorkflowExecutionStatus.RUNNING,
|
||||
WorkflowExecutionStatus.RETRYING_AFTER_ERROR,
|
||||
WorkflowExecutionStatus.CONTINUED_AS_NEW,
|
||||
],
|
||||
)
|
||||
|
|
@ -207,7 +196,7 @@ class TestListRemoteResumeSessions:
|
|||
previous = WorkflowExecutionWithoutResultResponse(
|
||||
workflow_name="vibe",
|
||||
execution_id="exec-1",
|
||||
status=WorkflowExecutionStatus.RETRYING_AFTER_ERROR,
|
||||
status=WorkflowExecutionStatus.FAILED,
|
||||
start_time=datetime(2026, 1, 1),
|
||||
end_time=datetime(2026, 1, 10),
|
||||
)
|
||||
|
|
|
|||
|
|
@ -346,7 +346,10 @@ class TestSessionLoaderFindLatestSession:
|
|||
assert result == valid_session
|
||||
|
||||
def test_find_latest_session_skips_unreadable_messages_file(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
create_test_session,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
|
|
@ -355,14 +358,28 @@ class TestSessionLoaderFindLatestSession:
|
|||
|
||||
unreadable_session = create_test_session(session_dir, "unreadab-session")
|
||||
unreadable_messages = unreadable_session / "messages.jsonl"
|
||||
unreadable_messages.chmod(0)
|
||||
|
||||
# chmod doesn't restrict root, so simulate an unreadable file by
|
||||
# patching Path.read_bytes (used under the hood by read_safe). This
|
||||
# keeps the test working in CI environments running as root.
|
||||
original_read_bytes = Path.read_bytes
|
||||
|
||||
def fake_read_bytes(self: Path) -> bytes:
|
||||
if self == unreadable_messages:
|
||||
raise PermissionError(f"Permission denied: {self}")
|
||||
return original_read_bytes(self)
|
||||
|
||||
monkeypatch.setattr(Path, "read_bytes", fake_read_bytes)
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
assert result == valid_session
|
||||
|
||||
def test_find_latest_session_skips_unreadable_metadata_file(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
self,
|
||||
session_config: SessionLoggingConfig,
|
||||
create_test_session,
|
||||
monkeypatch: pytest.MonkeyPatch,
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
|
|
@ -371,7 +388,15 @@ class TestSessionLoaderFindLatestSession:
|
|||
|
||||
unreadable_session = create_test_session(session_dir, "unreadab-session")
|
||||
unreadable_metadata = unreadable_session / "meta.json"
|
||||
unreadable_metadata.chmod(0)
|
||||
|
||||
original_read_bytes = Path.read_bytes
|
||||
|
||||
def fake_read_bytes(self: Path) -> bytes:
|
||||
if self == unreadable_metadata:
|
||||
raise PermissionError(f"Permission denied: {self}")
|
||||
return original_read_bytes(self)
|
||||
|
||||
monkeypatch.setattr(Path, "read_bytes", fake_read_bytes)
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
|
|
@ -436,6 +461,41 @@ class TestSessionLoaderFindSessionById:
|
|||
assert result is not None
|
||||
assert result == session_2
|
||||
|
||||
def test_find_session_by_id_filters_by_working_directory(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
session_a = create_test_session(
|
||||
session_dir,
|
||||
"abcd1234-session",
|
||||
working_directory=Path("/home/user/project-a"),
|
||||
)
|
||||
time.sleep(0.01)
|
||||
session_b = create_test_session(
|
||||
session_dir,
|
||||
"abcd1234-session",
|
||||
working_directory=Path("/home/user/project-b"),
|
||||
)
|
||||
|
||||
assert (
|
||||
SessionLoader.find_session_by_id(
|
||||
"abcd1234",
|
||||
session_config,
|
||||
working_directory=Path("/home/user/project-a"),
|
||||
)
|
||||
== session_a
|
||||
)
|
||||
assert (
|
||||
SessionLoader.find_session_by_id(
|
||||
"abcd1234",
|
||||
session_config,
|
||||
working_directory=Path("/home/user/project-c"),
|
||||
)
|
||||
is None
|
||||
)
|
||||
assert SessionLoader.find_session_by_id("abcd1234", session_config) == session_b
|
||||
|
||||
def test_find_session_by_id_no_match(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
|
|
|
|||
|
|
@ -11,6 +11,7 @@ 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
|
||||
|
|
@ -114,16 +115,12 @@ class TestSessionLoggerMetadata:
|
|||
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"
|
||||
# Mock combined git command
|
||||
git_mock = MagicMock()
|
||||
git_mock.returncode = 0
|
||||
git_mock.stdout = "abc123\nmain\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_subprocess.return_value = git_mock
|
||||
mock_getuser.return_value = "testuser"
|
||||
|
||||
session_id = "test-session-123"
|
||||
|
|
@ -149,7 +146,7 @@ class TestSessionLoggerMetadata:
|
|||
self, mock_getuser, mock_subprocess, session_config: SessionLoggingConfig
|
||||
) -> None:
|
||||
"""Test that session metadata handles git command errors gracefully."""
|
||||
# Mock git commands to fail
|
||||
# Mock combined git command to fail
|
||||
mock_subprocess.side_effect = FileNotFoundError("git not found")
|
||||
mock_getuser.return_value = "testuser"
|
||||
|
||||
|
|
@ -1002,3 +999,153 @@ class TestPersistLoops:
|
|||
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"
|
||||
)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue