v2.0.2
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>
This commit is contained in:
parent
bd3497b1c0
commit
9809cfc831
32 changed files with 622 additions and 305 deletions
|
|
@ -125,42 +125,6 @@ class TestAcpBashBasic:
|
|||
display = Bash.get_summary(args)
|
||||
assert display == "ls (timeout 10s)"
|
||||
|
||||
def test_parse_command_simple(self) -> None:
|
||||
tool = Bash(config=BashToolConfig(), state=AcpBashState())
|
||||
env, command, args = tool._parse_command("ls")
|
||||
assert env == []
|
||||
assert command == "ls"
|
||||
assert args == []
|
||||
|
||||
def test_parse_command_with_args(self) -> None:
|
||||
tool = Bash(config=BashToolConfig(), state=AcpBashState())
|
||||
env, command, args = tool._parse_command("ls -la src")
|
||||
assert env == []
|
||||
assert command == "ls"
|
||||
assert args == ["-la", "src"]
|
||||
|
||||
def test_parse_command_with_env(self) -> None:
|
||||
tool = Bash(config=BashToolConfig(), state=AcpBashState())
|
||||
env, command, args = tool._parse_command("NODE_ENV=test DEBUG=1 npm test")
|
||||
assert len(env) == 2
|
||||
assert env[0].name == "NODE_ENV"
|
||||
assert env[0].value == "test"
|
||||
assert env[1].name == "DEBUG"
|
||||
assert env[1].value == "1"
|
||||
assert command == "npm"
|
||||
assert args == ["test"]
|
||||
|
||||
def test_parse_command_with_env_value_contains_equals(self) -> None:
|
||||
tool = Bash(config=BashToolConfig(), state=AcpBashState())
|
||||
env, command, args = tool._parse_command(
|
||||
"PATH=/usr/bin:/usr/local/bin echo hello"
|
||||
)
|
||||
assert len(env) == 1
|
||||
assert env[0].name == "PATH"
|
||||
assert env[0].value == "/usr/bin:/usr/local/bin"
|
||||
assert command == "echo"
|
||||
assert args == ["hello"]
|
||||
|
||||
|
||||
class TestAcpBashExecution:
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -181,8 +145,7 @@ class TestAcpBashExecution:
|
|||
# Verify create_terminal was called correctly
|
||||
params = mock_client._last_create_params
|
||||
assert params["session_id"] == "test_session_123"
|
||||
assert params["command"] == "echo"
|
||||
assert params["args"] == ["hello"]
|
||||
assert params["command"] == "echo hello"
|
||||
assert params["cwd"] == str(Path.cwd()) # effective_workdir defaults to cwd
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -200,16 +163,7 @@ class TestAcpBashExecution:
|
|||
await collect_result(tool.run(args))
|
||||
|
||||
params = mock_client._last_create_params
|
||||
env = params["env"]
|
||||
assert env is not None
|
||||
assert (
|
||||
isinstance(env, list) and len(env) > 0 and isinstance(env[0], EnvVariable)
|
||||
)
|
||||
assert len(env) == 1
|
||||
assert env[0].name == "NODE_ENV"
|
||||
assert env[0].value == "test"
|
||||
assert params["command"] == "npm"
|
||||
assert params["args"] == ["run", "build"]
|
||||
assert params["command"] == "NODE_ENV=test npm run build"
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_run_with_nonzero_exit_code(self, mock_client: MockClient) -> None:
|
||||
|
|
|
|||
|
|
@ -25,7 +25,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.0.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.0.2"
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
|
@ -48,7 +48,7 @@ class TestACPInitialize:
|
|||
),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.0.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.0.2"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -5,7 +5,11 @@ import logging
|
|||
import pytest
|
||||
|
||||
from tests.cli.plan_offer.adapters.fake_whoami_gateway import FakeWhoAmIGateway
|
||||
from vibe.cli.plan_offer.decide_plan_offer import PlanOfferAction, decide_plan_offer
|
||||
from vibe.cli.plan_offer.decide_plan_offer import (
|
||||
PlanOfferAction,
|
||||
PlanType,
|
||||
decide_plan_offer,
|
||||
)
|
||||
from vibe.cli.plan_offer.ports.whoami_gateway import WhoAmIResponse
|
||||
|
||||
|
||||
|
|
@ -18,14 +22,15 @@ async def test_proposes_upgrade_without_call_when_api_key_is_empty() -> None:
|
|||
prompt_switching_to_pro_plan=False,
|
||||
)
|
||||
)
|
||||
action = await decide_plan_offer("", gateway)
|
||||
action, plan_type = await decide_plan_offer("", gateway)
|
||||
|
||||
assert action is PlanOfferAction.UPGRADE
|
||||
assert plan_type is PlanType.FREE
|
||||
assert gateway.calls == []
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
("response", "expected"),
|
||||
("response", "expected_action", "expected_plan_type"),
|
||||
[
|
||||
(
|
||||
WhoAmIResponse(
|
||||
|
|
@ -34,6 +39,7 @@ async def test_proposes_upgrade_without_call_when_api_key_is_empty() -> None:
|
|||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
PlanOfferAction.NONE,
|
||||
PlanType.PRO,
|
||||
),
|
||||
(
|
||||
WhoAmIResponse(
|
||||
|
|
@ -42,6 +48,7 @@ async def test_proposes_upgrade_without_call_when_api_key_is_empty() -> None:
|
|||
prompt_switching_to_pro_plan=False,
|
||||
),
|
||||
PlanOfferAction.UPGRADE,
|
||||
PlanType.FREE,
|
||||
),
|
||||
(
|
||||
WhoAmIResponse(
|
||||
|
|
@ -50,18 +57,22 @@ async def test_proposes_upgrade_without_call_when_api_key_is_empty() -> None:
|
|||
prompt_switching_to_pro_plan=True,
|
||||
),
|
||||
PlanOfferAction.SWITCH_TO_PRO_KEY,
|
||||
PlanType.PRO,
|
||||
),
|
||||
],
|
||||
ids=["with-a-pro-plan", "without-a-pro-plan", "with-a-non-pro-key"],
|
||||
)
|
||||
@pytest.mark.asyncio
|
||||
async def test_proposes_an_action_based_on_current_plan_status(
|
||||
response: WhoAmIResponse, expected: PlanOfferAction
|
||||
response: WhoAmIResponse,
|
||||
expected_action: PlanOfferAction,
|
||||
expected_plan_type: PlanType,
|
||||
) -> None:
|
||||
gateway = FakeWhoAmIGateway(response)
|
||||
action = await decide_plan_offer("api-key", gateway)
|
||||
action, plan_type = await decide_plan_offer("api-key", gateway)
|
||||
|
||||
assert action is expected
|
||||
assert action is expected_action
|
||||
assert plan_type is expected_plan_type
|
||||
assert gateway.calls == ["api-key"]
|
||||
|
||||
|
||||
|
|
@ -75,18 +86,20 @@ async def test_proposes_nothing_when_nothing_is_suggested() -> None:
|
|||
)
|
||||
)
|
||||
|
||||
action = await decide_plan_offer("api-key", gateway)
|
||||
action, plan_type = await decide_plan_offer("api-key", gateway)
|
||||
|
||||
assert action is PlanOfferAction.NONE
|
||||
assert plan_type is PlanType.UNKNOWN
|
||||
assert gateway.calls == ["api-key"]
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_proposes_upgrade_when_api_key_is_unauthorized() -> None:
|
||||
gateway = FakeWhoAmIGateway(unauthorized=True)
|
||||
action = await decide_plan_offer("bad-key", gateway)
|
||||
action, plan_type = await decide_plan_offer("bad-key", gateway)
|
||||
|
||||
assert action is PlanOfferAction.UPGRADE
|
||||
assert plan_type is PlanType.FREE
|
||||
assert gateway.calls == ["bad-key"]
|
||||
|
||||
|
||||
|
|
@ -96,8 +109,9 @@ async def test_proposes_none_and_logs_warning_when_gateway_error_occurs(
|
|||
) -> None:
|
||||
gateway = FakeWhoAmIGateway(error=True)
|
||||
with caplog.at_level(logging.WARNING):
|
||||
action = await decide_plan_offer("api-key", gateway)
|
||||
action, plan_type = await decide_plan_offer("api-key", gateway)
|
||||
|
||||
assert action is PlanOfferAction.NONE
|
||||
assert plan_type is PlanType.UNKNOWN
|
||||
assert gateway.calls == ["api-key"]
|
||||
assert "Failed to fetch plan status." in caplog.text
|
||||
|
|
|
|||
61
tests/core/test_config_load_dotenv.py
Normal file
61
tests/core/test_config_load_dotenv.py
Normal file
|
|
@ -0,0 +1,61 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
from vibe.core.config import load_dotenv_values
|
||||
|
||||
|
||||
def _write_env_file(path: Path, content: str) -> None:
|
||||
path.write_text(content, encoding="utf-8")
|
||||
|
||||
|
||||
def test_skips_missing_file(tmp_path: Path) -> None:
|
||||
environ = {"EXISTING": "1"}
|
||||
missing_path = tmp_path / "missing.env"
|
||||
|
||||
load_dotenv_values(env_path=missing_path, environ=environ)
|
||||
|
||||
assert environ == {"EXISTING": "1"}
|
||||
|
||||
|
||||
def test_sets_and_overrides_values(tmp_path: Path) -> None:
|
||||
env_path = tmp_path / ".env"
|
||||
_write_env_file(
|
||||
env_path,
|
||||
"\n".join([
|
||||
"MISTRAL_API_KEY=new-key",
|
||||
"HTTPS_PROXY=https://local-proxy:8080",
|
||||
"OTHER=from-env",
|
||||
"NEW_KEY=added",
|
||||
"FOO=replace",
|
||||
])
|
||||
+ "\n",
|
||||
)
|
||||
environ = {
|
||||
"MISTRAL_API_KEY": "old-key",
|
||||
"HTTPS_PROXY": "old-https",
|
||||
"OTHER": "keep",
|
||||
"FOO": "keep",
|
||||
}
|
||||
|
||||
load_dotenv_values(env_path=env_path, environ=environ)
|
||||
|
||||
assert environ["MISTRAL_API_KEY"] == "new-key"
|
||||
assert environ["HTTPS_PROXY"] == "https://local-proxy:8080"
|
||||
assert environ["OTHER"] == "from-env"
|
||||
assert environ["NEW_KEY"] == "added"
|
||||
assert environ["FOO"] == "replace"
|
||||
|
||||
|
||||
def test_ignores_empty_values(tmp_path: Path) -> None:
|
||||
env_path = tmp_path / ".env"
|
||||
_write_env_file(
|
||||
env_path, "\n".join(["EMPTY=", "MISTRAL_API_KEY=", "NO_VALUE"]) + "\n"
|
||||
)
|
||||
environ: dict[str, str] = {}
|
||||
|
||||
load_dotenv_values(env_path=env_path, environ=environ)
|
||||
|
||||
assert "EMPTY" not in environ
|
||||
assert "MISTRAL_API_KEY" not in environ
|
||||
assert "NO_VALUE" not in environ
|
||||
|
|
@ -1,51 +0,0 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from contextlib import contextmanager
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
|
||||
import tomli_w
|
||||
|
||||
from vibe.core import config
|
||||
from vibe.core.config import VibeConfig
|
||||
|
||||
|
||||
def _restore_dump_config(config_file: Path):
|
||||
original_dump_config = VibeConfig.dump_config
|
||||
|
||||
def real_dump_config(cls, config_dict: dict) -> None:
|
||||
try:
|
||||
with config_file.open("wb") as f:
|
||||
tomli_w.dump(config_dict, f)
|
||||
except OSError:
|
||||
config_file.write_text(
|
||||
"\n".join(
|
||||
f"{k} = {v!r}" for k, v in config_dict.items() if v is not None
|
||||
),
|
||||
encoding="utf-8",
|
||||
)
|
||||
|
||||
VibeConfig.dump_config = classmethod(real_dump_config) # type: ignore[assignment]
|
||||
return original_dump_config
|
||||
|
||||
|
||||
@contextmanager
|
||||
def _migrate_config_file(tmp_path: Path, content: str):
|
||||
config_file = tmp_path / "config.toml"
|
||||
config_file.write_text(content, encoding="utf-8")
|
||||
|
||||
original_config_file = config.CONFIG_FILE
|
||||
original_dump_config = _restore_dump_config(config_file)
|
||||
|
||||
try:
|
||||
config.CONFIG_FILE = config_file
|
||||
VibeConfig._migrate()
|
||||
yield config_file
|
||||
finally:
|
||||
config.CONFIG_FILE = original_config_file
|
||||
VibeConfig.dump_config = original_dump_config
|
||||
|
||||
|
||||
def _load_migrated_config(config_file: Path) -> dict:
|
||||
with config_file.open("rb") as f:
|
||||
return tomllib.load(f)
|
||||
|
|
@ -206,6 +206,83 @@ class TestSessionLoaderFindLatestSession:
|
|||
assert result is not None
|
||||
assert result == valid_session
|
||||
|
||||
def test_find_latest_session_skips_empty_messages_file(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
valid_session = create_test_session(session_dir, "valid123-session")
|
||||
time.sleep(0.01)
|
||||
|
||||
empty_session = create_test_session(session_dir, "emptymss-session")
|
||||
(empty_session / "messages.jsonl").write_text("")
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
assert result == valid_session
|
||||
|
||||
def test_find_latest_session_skips_messages_json_not_dict(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
valid_session = create_test_session(session_dir, "valid123-session")
|
||||
time.sleep(0.01)
|
||||
|
||||
invalid_session = create_test_session(session_dir, "msglist-session")
|
||||
(invalid_session / "messages.jsonl").write_text("[]\n")
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
assert result == valid_session
|
||||
|
||||
def test_find_latest_session_skips_metadata_json_not_dict(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
valid_session = create_test_session(session_dir, "valid123-session")
|
||||
time.sleep(0.01)
|
||||
|
||||
invalid_session = create_test_session(session_dir, "metalist-session")
|
||||
(invalid_session / "meta.json").write_text("[]")
|
||||
|
||||
result = SessionLoader.find_latest_session(session_config)
|
||||
assert result is not None
|
||||
assert result == valid_session
|
||||
|
||||
def test_find_latest_session_skips_unreadable_messages_file(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
valid_session = create_test_session(session_dir, "valid123-session")
|
||||
time.sleep(0.01)
|
||||
|
||||
unreadable_session = create_test_session(session_dir, "unreadab-session")
|
||||
unreadable_messages = unreadable_session / "messages.jsonl"
|
||||
unreadable_messages.chmod(0)
|
||||
|
||||
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
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
|
||||
valid_session = create_test_session(session_dir, "valid123-session")
|
||||
time.sleep(0.01)
|
||||
|
||||
unreadable_session = create_test_session(session_dir, "unreadab-session")
|
||||
unreadable_metadata = unreadable_session / "meta.json"
|
||||
unreadable_metadata.chmod(0)
|
||||
|
||||
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(
|
||||
|
|
@ -285,6 +362,27 @@ class TestSessionLoaderFindSessionById:
|
|||
assert result is None
|
||||
|
||||
|
||||
class TestSessionLoaderDoesSessionExist:
|
||||
def test_does_session_exist_no_messages(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = create_test_session(session_dir, "test-session-123")
|
||||
(session_folder / "messages.jsonl").unlink()
|
||||
|
||||
result = SessionLoader.does_session_exist("test-session-123", session_config)
|
||||
assert result is None
|
||||
|
||||
def test_does_session_exist_success(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
) -> None:
|
||||
session_dir = Path(session_config.save_dir)
|
||||
session_folder = create_test_session(session_dir, "test-session-123")
|
||||
|
||||
result = SessionLoader.does_session_exist("test-session-123", session_config)
|
||||
assert result == session_folder
|
||||
|
||||
|
||||
class TestSessionLoaderLoadSession:
|
||||
def test_load_session_success(
|
||||
self, session_config: SessionLoggingConfig, create_test_session
|
||||
|
|
|
|||
|
|
@ -208,8 +208,7 @@ class TestSessionLoggerSaveInteraction:
|
|||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
# Test that save_interaction returns a path when enabled
|
||||
result = await logger.save_interaction(
|
||||
await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
|
|
@ -217,11 +216,7 @@ class TestSessionLoggerSaveInteraction:
|
|||
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
|
||||
# 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"
|
||||
|
|
@ -229,7 +224,6 @@ class TestSessionLoggerSaveInteraction:
|
|||
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
|
||||
|
|
@ -261,7 +255,7 @@ class TestSessionLoggerSaveInteraction:
|
|||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
result = await logger.save_interaction(
|
||||
await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
|
|
@ -269,10 +263,9 @@ class TestSessionLoggerSaveInteraction:
|
|||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
assert result is not None
|
||||
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
|
||||
|
|
@ -280,6 +273,7 @@ class TestSessionLoggerSaveInteraction:
|
|||
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]
|
||||
|
|
@ -330,7 +324,7 @@ class TestSessionLoggerSaveInteraction:
|
|||
steps=2, session_prompt_tokens=20, session_completion_tokens=40
|
||||
)
|
||||
|
||||
result = await logger.save_interaction(
|
||||
await logger.save_interaction(
|
||||
messages=all_messages,
|
||||
stats=updated_stats,
|
||||
base_config=mock_vibe_config,
|
||||
|
|
@ -338,17 +332,78 @@ class TestSessionLoggerSaveInteraction:
|
|||
agent_profile=mock_agent_profile,
|
||||
)
|
||||
|
||||
# Verify that the result is not None
|
||||
assert result is not None
|
||||
|
||||
# Verify that metadata was updated
|
||||
# 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,
|
||||
|
|
@ -371,7 +426,7 @@ class TestSessionLoggerSaveInteraction:
|
|||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
result = await logger.save_interaction(
|
||||
await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
|
|
@ -379,13 +434,10 @@ class TestSessionLoggerSaveInteraction:
|
|||
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
|
||||
# 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
|
||||
|
|
@ -393,6 +445,11 @@ class TestSessionLoggerSaveInteraction:
|
|||
assert metadata["stats"]["steps"] == stats.steps
|
||||
assert metadata["title"] == "Untitled session"
|
||||
|
||||
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,
|
||||
|
|
@ -417,7 +474,7 @@ class TestSessionLoggerSaveInteraction:
|
|||
steps=1, session_prompt_tokens=10, session_completion_tokens=20
|
||||
)
|
||||
|
||||
result = await logger.save_interaction(
|
||||
await logger.save_interaction(
|
||||
messages=messages,
|
||||
stats=stats,
|
||||
base_config=mock_vibe_config,
|
||||
|
|
@ -425,13 +482,10 @@ class TestSessionLoggerSaveInteraction:
|
|||
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
|
||||
# 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
|
||||
|
|
@ -440,6 +494,11 @@ class TestSessionLoggerSaveInteraction:
|
|||
expected_title = long_message[:50] + "…"
|
||||
assert metadata["title"] == expected_title
|
||||
|
||||
messages_file = logger.session_dir / "messages.jsonl"
|
||||
assert messages_file.exists()
|
||||
with open(messages_file) as f:
|
||||
assert len(f.readlines()) == 2
|
||||
|
||||
|
||||
class TestSessionLoggerResetSession:
|
||||
def test_reset_session(self, session_config: SessionLoggingConfig) -> None:
|
||||
|
|
|
|||
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 20 KiB After Width: | Height: | Size: 19 KiB |
File diff suppressed because one or more lines are too long
|
Before Width: | Height: | Size: 23 KiB After Width: | Height: | Size: 22 KiB |
|
|
@ -1,9 +1,11 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from collections.abc import Callable
|
||||
from http import HTTPStatus
|
||||
from typing import cast
|
||||
from unittest.mock import AsyncMock
|
||||
|
||||
import httpx
|
||||
import pytest
|
||||
|
||||
from tests.mock.utils import mock_llm_chunk
|
||||
|
|
@ -11,6 +13,7 @@ from tests.stubs.fake_backend import FakeBackend
|
|||
from vibe.core.agent_loop import AgentLoop
|
||||
from vibe.core.agents.models import BuiltinAgentName
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
from vibe.core.llm.exceptions import BackendErrorBuilder
|
||||
from vibe.core.middleware import (
|
||||
ConversationContext,
|
||||
MiddlewareAction,
|
||||
|
|
@ -25,6 +28,7 @@ from vibe.core.types import (
|
|||
AssistantEvent,
|
||||
FunctionCall,
|
||||
LLMMessage,
|
||||
RateLimitError,
|
||||
ReasoningEvent,
|
||||
Role,
|
||||
ToolCall,
|
||||
|
|
@ -369,7 +373,7 @@ async def test_act_handles_user_cancellation_during_streaming() -> None:
|
|||
assert events[-1].skipped is True
|
||||
assert events[-1].skip_reason is not None
|
||||
assert "<user_cancellation>" in events[-1].skip_reason
|
||||
assert agent.session_logger.save_interaction.await_count == 1
|
||||
assert agent.session_logger.save_interaction.await_count >= 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -388,6 +392,34 @@ async def test_act_flushes_and_logs_when_streaming_errors(observer_capture) -> N
|
|||
assert agent.session_logger.save_interaction.await_count == 1
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rate_limit(observer_capture) -> None:
|
||||
observed, observer = observer_capture
|
||||
response = httpx.Response(HTTPStatus.TOO_MANY_REQUESTS)
|
||||
backend_error = BackendErrorBuilder.build_http_error(
|
||||
provider="mistral",
|
||||
endpoint="test",
|
||||
response=response,
|
||||
headers=None,
|
||||
model="test-model",
|
||||
messages=[],
|
||||
temperature=0.0,
|
||||
has_tools=False,
|
||||
tool_choice=None,
|
||||
)
|
||||
backend = FakeBackend(exception_to_raise=backend_error)
|
||||
agent = AgentLoop(
|
||||
make_config(), backend=backend, message_observer=observer, enable_streaming=True
|
||||
)
|
||||
agent.session_logger.save_interaction = AsyncMock(return_value=None)
|
||||
|
||||
with pytest.raises(RateLimitError):
|
||||
[_ async for _ in agent.act("Trigger rate limit failure while streaming")]
|
||||
|
||||
assert [role for role, _ in observed] == [Role.system, Role.user]
|
||||
assert agent.session_logger.save_interaction.await_count == 1
|
||||
|
||||
|
||||
def _snapshot_events(events: list) -> list[tuple[str, str]]:
|
||||
return [
|
||||
(type(e).__name__, e.content)
|
||||
|
|
|
|||
|
|
@ -1,5 +1,7 @@
|
|||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from tests.mock.utils import mock_llm_chunk
|
||||
|
|
@ -16,6 +18,8 @@ from vibe.core.agents.models import (
|
|||
_deep_merge,
|
||||
)
|
||||
from vibe.core.config import SessionLoggingConfig, VibeConfig
|
||||
from vibe.core.paths.config_paths import ConfigPath
|
||||
from vibe.core.paths.global_paths import GlobalPath
|
||||
from vibe.core.tools.base import ToolPermission
|
||||
from vibe.core.types import (
|
||||
FunctionCall,
|
||||
|
|
@ -164,6 +168,43 @@ class TestAgentProfile:
|
|||
}
|
||||
|
||||
|
||||
class TestAgentApplyToConfig:
|
||||
def test_custom_prompt_found_in_global_when_missing_from_project(
|
||||
self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch
|
||||
) -> None:
|
||||
"""Regression test for https://github.com/mistralai/mistral-vibe/issues/288
|
||||
|
||||
When a custom prompt .md file is absent from the project-local prompts
|
||||
directory, the system_prompt property should fall back to the global
|
||||
~/.vibe/prompts/ directory and load the file from there.
|
||||
"""
|
||||
project_prompts = tmp_path / "project" / ".vibe" / "prompts"
|
||||
project_prompts.mkdir(parents=True)
|
||||
|
||||
global_prompts = tmp_path / "home" / ".vibe" / "prompts"
|
||||
global_prompts.mkdir(parents=True)
|
||||
(global_prompts / "cc.md").write_text("Global custom prompt")
|
||||
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.config.PROMPTS_DIR", ConfigPath(lambda: project_prompts)
|
||||
)
|
||||
monkeypatch.setattr(
|
||||
"vibe.core.config.GLOBAL_PROMPTS_DIR", GlobalPath(lambda: global_prompts)
|
||||
)
|
||||
|
||||
base = VibeConfig(include_project_context=False, include_prompt_detail=False)
|
||||
agent = AgentProfile(
|
||||
name="cc",
|
||||
display_name="Cc",
|
||||
description="",
|
||||
safety=AgentSafety.NEUTRAL,
|
||||
overrides={"system_prompt_id": "cc"},
|
||||
)
|
||||
result = agent.apply_to_config(base)
|
||||
assert result.system_prompt_id == "cc"
|
||||
assert result.system_prompt == "Global custom prompt"
|
||||
|
||||
|
||||
class TestAgentProfileOverrides:
|
||||
def test_default_agent_has_no_overrides(self) -> None:
|
||||
assert BUILTIN_AGENTS[BuiltinAgentName.DEFAULT].overrides == {}
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue