v2.7.2 (#556)
Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Vincent G <10739306+VinceOPS@users.noreply.github.com> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
54b9a17457
commit
9c1c32e058
28 changed files with 587 additions and 179 deletions
|
|
@ -28,7 +28,7 @@ class TestACPInitialize:
|
|||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.2"
|
||||
)
|
||||
|
||||
assert response.auth_methods == []
|
||||
|
|
@ -52,7 +52,7 @@ class TestACPInitialize:
|
|||
session_capabilities=SessionCapabilities(list=SessionListCapabilities()),
|
||||
)
|
||||
assert response.agent_info == Implementation(
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.1"
|
||||
name="@mistralai/mistral-vibe", title="Mistral Vibe", version="2.7.2"
|
||||
)
|
||||
|
||||
assert response.auth_methods is not None
|
||||
|
|
|
|||
|
|
@ -12,8 +12,9 @@ from tests.snapshots.snap_compare import SnapCompare
|
|||
from tests.stubs.fake_audio_player import FakeAudioPlayer
|
||||
from tests.stubs.fake_backend import FakeBackend
|
||||
from tests.stubs.fake_tts_client import FakeTTSClient
|
||||
from vibe.cli.narrator_manager import NarratorManager, NarratorState
|
||||
import vibe.cli.textual_ui.widgets.narrator_status as narrator_status_mod
|
||||
from vibe.cli.textual_ui.widgets.narrator_status import NarratorState, NarratorStatus
|
||||
from vibe.cli.textual_ui.widgets.narrator_status import NarratorStatus
|
||||
from vibe.cli.turn_summary import TurnSummaryTracker
|
||||
|
||||
narrator_status_mod.SHRINK_FRAMES = "█"
|
||||
|
|
@ -65,6 +66,16 @@ class NarratorFlowApp(BaseSnapshotTestApp):
|
|||
mock_llm_chunk(content="Summary of the conversation")
|
||||
)
|
||||
self.tts_gate = GatedTTSClient()
|
||||
self.fake_audio_player = FakeAudioPlayer()
|
||||
narrator_manager = NarratorManager(
|
||||
config_getter=_narrator_config, audio_player=self.fake_audio_player
|
||||
)
|
||||
# Override turn_summary and tts_client with gated test doubles.
|
||||
narrator_manager._turn_summary = TurnSummaryTracker(
|
||||
backend=self.summary_gate, model=_TEST_MODEL
|
||||
)
|
||||
narrator_manager._turn_summary.on_summary = narrator_manager._on_turn_summary
|
||||
narrator_manager._tts_client = self.tts_gate
|
||||
super().__init__(
|
||||
config=_narrator_config(),
|
||||
backend=FakeBackend(
|
||||
|
|
@ -74,13 +85,7 @@ class NarratorFlowApp(BaseSnapshotTestApp):
|
|||
completion_tokens=2_500,
|
||||
)
|
||||
),
|
||||
)
|
||||
self._tts_client = self.tts_gate
|
||||
self._audio_player = FakeAudioPlayer()
|
||||
self._turn_summary = TurnSummaryTracker(
|
||||
backend=self.summary_gate,
|
||||
model=_TEST_MODEL,
|
||||
on_summary=self._on_turn_summary,
|
||||
narrator_manager=narrator_manager,
|
||||
)
|
||||
|
||||
|
||||
|
|
@ -91,7 +96,7 @@ def test_snapshot_narrator_summarizing(snap_compare: SnapCompare) -> None:
|
|||
await pilot.press(*"Hello")
|
||||
await pilot.press("enter")
|
||||
await pilot.pause(0.5)
|
||||
# end_turn has fired, SUMMARIZING is set, summary backend is gated
|
||||
# on_turn_end has fired, SUMMARIZING is set, summary backend is gated
|
||||
assert app.summary_gate._gate.is_set() is False
|
||||
# Freeze animation at frame 0 for deterministic snapshot
|
||||
app.query_one(NarratorStatus)._stop_timer()
|
||||
|
|
@ -137,8 +142,9 @@ def test_snapshot_narrator_idle_after_speaking(snap_compare: SnapCompare) -> Non
|
|||
app.tts_gate.release()
|
||||
await pilot.pause(0.2)
|
||||
# Simulate playback finishing (same thread, so call directly)
|
||||
app._audio_player.stop()
|
||||
app._set_narrator_state(NarratorState.IDLE)
|
||||
app.fake_audio_player.stop()
|
||||
narrator = cast(NarratorManager, app._narrator_manager)
|
||||
narrator._set_state(NarratorState.IDLE)
|
||||
await pilot.pause(0.2)
|
||||
|
||||
assert snap_compare(
|
||||
|
|
|
|||
|
|
@ -9,6 +9,7 @@ from vibe.cli.history_manager import HistoryManager
|
|||
from vibe.cli.textual_ui.app import VibeApp
|
||||
from vibe.cli.textual_ui.widgets.chat_input.body import ChatInputBody
|
||||
from vibe.cli.textual_ui.widgets.chat_input.container import ChatInputContainer
|
||||
from vibe.cli.textual_ui.widgets.messages import UserMessage
|
||||
|
||||
|
||||
@pytest.fixture
|
||||
|
|
@ -112,6 +113,29 @@ async def test_ui_does_not_prevent_arrow_down_to_move_cursor_to_bottom_lines(
|
|||
assert final_row == 1, f"cursor is still on line {final_row}."
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_alt_left_and_alt_right_move_by_word(vibe_app: VibeApp) -> None:
|
||||
async with vibe_app.run_test() as pilot:
|
||||
chat_input = vibe_app.query_one(ChatInputContainer)
|
||||
textarea = chat_input.input_widget
|
||||
assert textarea is not None
|
||||
|
||||
await pilot.press(*"hello brave world")
|
||||
assert textarea.cursor_location == (0, len("hello brave world"))
|
||||
|
||||
await pilot.press("alt+left")
|
||||
assert textarea.cursor_location == (0, len("hello brave "))
|
||||
|
||||
await pilot.press("alt+left")
|
||||
assert textarea.cursor_location == (0, len("hello "))
|
||||
|
||||
await pilot.press("alt+right")
|
||||
assert textarea.cursor_location == (0, len("hello brave"))
|
||||
|
||||
assert chat_input.value == "hello brave world"
|
||||
assert len(vibe_app.query(UserMessage)) == 0
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_ui_resumes_arrow_down_after_manual_move(
|
||||
vibe_app: VibeApp, tmp_path: Path
|
||||
|
|
|
|||
|
|
@ -50,7 +50,7 @@ async def test_rewind_highlights_last_user_message() -> None:
|
|||
await pilot.pause(0.1)
|
||||
|
||||
assert app._rewind_highlighted_widget is not None
|
||||
assert app._rewind_highlighted_widget._content == "world"
|
||||
assert app._rewind_highlighted_widget.get_content() == "world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -67,7 +67,7 @@ async def test_rewind_navigates_to_previous_message() -> None:
|
|||
await pilot.pause(0.1)
|
||||
|
||||
assert app._rewind_highlighted_widget is not None
|
||||
assert app._rewind_highlighted_widget._content == "hello"
|
||||
assert app._rewind_highlighted_widget.get_content() == "hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -88,7 +88,7 @@ async def test_rewind_navigates_down() -> None:
|
|||
await pilot.pause(0.1)
|
||||
|
||||
assert app._rewind_highlighted_widget is not None
|
||||
assert app._rewind_highlighted_widget._content == "world"
|
||||
assert app._rewind_highlighted_widget.get_content() == "world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -123,7 +123,7 @@ async def test_rewind_ctrl_p_n_alternate_bindings() -> None:
|
|||
|
||||
assert app._rewind_mode is True
|
||||
assert app._rewind_highlighted_widget is not None
|
||||
assert app._rewind_highlighted_widget._content == "world"
|
||||
assert app._rewind_highlighted_widget.get_content() == "world"
|
||||
|
||||
# ctrl+p again goes to previous
|
||||
await pilot.press("ctrl+p")
|
||||
|
|
@ -131,7 +131,7 @@ async def test_rewind_ctrl_p_n_alternate_bindings() -> None:
|
|||
await pilot.pause(0.1)
|
||||
|
||||
assert app._rewind_highlighted_widget is not None
|
||||
assert app._rewind_highlighted_widget._content == "hello"
|
||||
assert app._rewind_highlighted_widget.get_content() == "hello"
|
||||
|
||||
# ctrl+n goes back
|
||||
await pilot.press("ctrl+n")
|
||||
|
|
@ -139,7 +139,7 @@ async def test_rewind_ctrl_p_n_alternate_bindings() -> None:
|
|||
await pilot.pause(0.1)
|
||||
|
||||
assert app._rewind_highlighted_widget is not None
|
||||
assert app._rewind_highlighted_widget._content == "world"
|
||||
assert app._rewind_highlighted_widget.get_content() == "world"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
@ -180,7 +180,7 @@ async def test_rewind_removes_messages_after_selected() -> None:
|
|||
await pilot.pause(0.1)
|
||||
|
||||
assert app._rewind_highlighted_widget is not None
|
||||
assert app._rewind_highlighted_widget._content == "second"
|
||||
assert app._rewind_highlighted_widget.get_content() == "second"
|
||||
|
||||
# Confirm
|
||||
await pilot.press("enter")
|
||||
|
|
@ -193,7 +193,37 @@ async def test_rewind_removes_messages_after_selected() -> None:
|
|||
child for child in messages_area.children if isinstance(child, UserMessage)
|
||||
]
|
||||
assert len(user_widgets) == 1
|
||||
assert user_widgets[0]._content == "first"
|
||||
assert user_widgets[0].get_content() == "first"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
async def test_rewind_skips_command_messages() -> None:
|
||||
"""Slash-command echo messages (message_index=None) are not rewind-selectable."""
|
||||
app = _make_app()
|
||||
async with app.run_test() as pilot:
|
||||
await _send_messages(pilot, ["hello"])
|
||||
|
||||
# Simulate a slash command inserting a UserMessage without message_index
|
||||
await app._mount_and_scroll(UserMessage("/model"))
|
||||
await pilot.pause(0.1)
|
||||
|
||||
await _send_messages(pilot, ["world"])
|
||||
|
||||
# First alt+up should land on "world", not the command message
|
||||
await pilot.press("alt+up")
|
||||
await pilot.app.workers.wait_for_complete()
|
||||
await pilot.pause(0.1)
|
||||
|
||||
assert app._rewind_highlighted_widget is not None
|
||||
assert app._rewind_highlighted_widget.get_content() == "world"
|
||||
|
||||
# Second alt+up should land on "hello", skipping the command message
|
||||
await pilot.press("alt+up")
|
||||
await pilot.app.workers.wait_for_complete()
|
||||
await pilot.pause(0.1)
|
||||
|
||||
assert app._rewind_highlighted_widget is not None
|
||||
assert app._rewind_highlighted_widget.get_content() == "hello"
|
||||
|
||||
|
||||
@pytest.mark.asyncio
|
||||
|
|
|
|||
|
|
@ -208,3 +208,66 @@ class TestResolvePermissionWindowsSyntax:
|
|||
)
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.ALWAYS
|
||||
|
||||
|
||||
class TestDenylistWordBoundary:
|
||||
"""Verify denylist matches whole command names, not prefixes."""
|
||||
|
||||
def _make_bash(self, **kwargs) -> Bash:
|
||||
config = BashToolConfig(**kwargs)
|
||||
return Bash(config=config, state=BaseToolState())
|
||||
|
||||
def test_vi_blocks_vi_exact(self):
|
||||
bash_tool = self._make_bash(denylist=["vi"])
|
||||
result = bash_tool.resolve_permission(BashArgs(command="vi"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.NEVER
|
||||
|
||||
def test_vi_blocks_vi_with_args(self):
|
||||
bash_tool = self._make_bash(denylist=["vi"])
|
||||
result = bash_tool.resolve_permission(BashArgs(command="vi file.txt"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.NEVER
|
||||
|
||||
def test_vi_does_not_block_vibe(self):
|
||||
bash_tool = self._make_bash(denylist=["vi"])
|
||||
result = bash_tool.resolve_permission(BashArgs(command="vibe -p hello"))
|
||||
assert result is None or result.permission is not ToolPermission.NEVER
|
||||
|
||||
def test_multiword_pattern_still_works(self):
|
||||
bash_tool = self._make_bash(denylist=["bash -i"])
|
||||
result = bash_tool.resolve_permission(BashArgs(command="bash -i"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.NEVER
|
||||
|
||||
def test_multiword_pattern_with_trailing_args(self):
|
||||
bash_tool = self._make_bash(denylist=["bash -i"])
|
||||
result = bash_tool.resolve_permission(BashArgs(command="bash -i extra"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.permission is ToolPermission.NEVER
|
||||
|
||||
def test_multiword_pattern_does_not_match_partial(self):
|
||||
bash_tool = self._make_bash(denylist=["bash -i"])
|
||||
result = bash_tool.resolve_permission(BashArgs(command="bash -init"))
|
||||
assert result is None or result.permission is not ToolPermission.NEVER
|
||||
|
||||
def test_deny_reason_is_set(self):
|
||||
bash_tool = self._make_bash(denylist=["vim"])
|
||||
result = bash_tool.resolve_permission(BashArgs(command="vim file.txt"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.reason is not None
|
||||
assert "vim" in result.reason
|
||||
|
||||
def test_standalone_deny_reason_is_set(self):
|
||||
bash_tool = self._make_bash(denylist_standalone=["python"])
|
||||
result = bash_tool.resolve_permission(BashArgs(command="python"))
|
||||
assert isinstance(result, PermissionContext)
|
||||
assert result.reason is not None
|
||||
assert result.permission is ToolPermission.NEVER
|
||||
assert "python" in result.reason
|
||||
assert "standalone" in result.reason
|
||||
|
||||
def test_allowlist_does_not_match_prefix(self):
|
||||
bash_tool = self._make_bash(allowlist=["cat"])
|
||||
result = bash_tool.resolve_permission(BashArgs(command="catalog"))
|
||||
assert result is not None and result.permission is not ToolPermission.ALWAYS
|
||||
|
|
|
|||
|
|
@ -430,6 +430,21 @@ class TestMCPRegistry:
|
|||
|
||||
assert len(registry._cache) == 0
|
||||
|
||||
def test_count_loaded_excludes_failed_servers(self):
|
||||
registry = MCPRegistry()
|
||||
ok_srv = self._make_http_server("ok", url="http://ok:1")
|
||||
fail_srv = self._make_http_server("fail", url="http://fail:2")
|
||||
|
||||
proxy = create_mcp_http_proxy_tool_class(
|
||||
url="http://ok:1", remote=RemoteTool(name="t"), alias="ok"
|
||||
)
|
||||
registry._cache[registry._server_key(ok_srv)] = {proxy.get_name(): proxy}
|
||||
|
||||
assert registry.count_loaded([ok_srv, fail_srv]) == 1
|
||||
assert registry.count_loaded([ok_srv]) == 1
|
||||
assert registry.count_loaded([fail_srv]) == 0
|
||||
assert registry.count_loaded([]) == 0
|
||||
|
||||
def test_cache_survives_multiple_get_tools_calls(self):
|
||||
registry = MCPRegistry()
|
||||
srv = self._make_http_server("stable")
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue