v2.9.0 (#641)
Co-authored-by: Antoine <33425718+anth2o@users.noreply.github.com> Co-authored-by: Bastien <bastien.baret@gmail.com> Co-authored-by: Clément Sirieix <clement.sirieix@mistral.ai> Co-authored-by: Kim-Adeline Miguel <51720070+kimadeline@users.noreply.github.com> Co-authored-by: Mathias Gesbert <mathias.gesbert@mistral.ai> Co-authored-by: Maxime Dolores <maxime.dolores@ext.mistral.ai> Co-authored-by: Michel Thomazo <51709227+michelTho@users.noreply.github.com> Co-authored-by: Nelson PROIA <144663685+Nelson-PROIA@users.noreply.github.com> Co-authored-by: Pierre Rossinès <pierre.rossines@mistral.ai> Co-authored-by: Quentin <quentin.torroba@mistral.ai> Co-authored-by: Robin Gullo <robin.gullo@mistral.ai> Co-authored-by: Mistral Vibe <vibe@mistral.ai>
This commit is contained in:
parent
a83c81ecf5
commit
632ea8c032
253 changed files with 13965 additions and 2525 deletions
|
|
@ -49,15 +49,23 @@ def strip_ansi(text: str) -> str:
|
|||
return re.sub(r"\x1b\[[0-9;?]*[ -/]*[@-~]|\x1b\][^\x07]*\x07", "", text)
|
||||
|
||||
|
||||
def poll_until(predicate: Callable[[], bool], timeout: float, message: str) -> None:
|
||||
start = time.monotonic()
|
||||
while time.monotonic() - start < timeout:
|
||||
if predicate():
|
||||
return
|
||||
time.sleep(0.05)
|
||||
raise AssertionError(message)
|
||||
|
||||
|
||||
def wait_for_request_count(
|
||||
request_count_getter: Callable[[], int], expected_count: int, timeout: float
|
||||
) -> None:
|
||||
start = time.monotonic()
|
||||
while time.monotonic() - start < timeout:
|
||||
if request_count_getter() >= expected_count:
|
||||
return
|
||||
time.sleep(0.05)
|
||||
raise AssertionError(f"Timed out waiting for {expected_count} backend request(s).")
|
||||
poll_until(
|
||||
lambda: request_count_getter() >= expected_count,
|
||||
timeout,
|
||||
f"Timed out waiting for {expected_count} backend request(s).",
|
||||
)
|
||||
|
||||
|
||||
def wait_for_main_screen(child: pexpect.spawn, timeout: float = 20.0) -> None:
|
||||
|
|
@ -84,3 +92,24 @@ def wait_for_rendered_text(
|
|||
raise AssertionError(
|
||||
f"Timed out waiting for rendered text: {needle!r}\n\nRendered tail:\n{rendered_tail}"
|
||||
)
|
||||
|
||||
|
||||
def send_ctrl_c_until_quit_confirmation(
|
||||
child: pexpect.spawn, captured: io.StringIO, timeout: float = 3
|
||||
) -> None:
|
||||
"""Send Ctrl+C and wait for quit confirmation prompt. Retries if first Ctrl+C interrupts."""
|
||||
start = time.monotonic()
|
||||
while time.monotonic() - start < timeout:
|
||||
child.sendcontrol("c")
|
||||
try:
|
||||
child.expect(ansi_tolerant_pattern("Press Ctrl+C again to quit"), timeout=2)
|
||||
# Confirmation prompt appeared, send second Ctrl+C
|
||||
child.sendcontrol("c")
|
||||
return
|
||||
except pexpect.TIMEOUT:
|
||||
# First Ctrl+C may have interrupted something, try again
|
||||
continue
|
||||
rendered_tail = strip_ansi(captured.getvalue())[-1200:]
|
||||
raise AssertionError(
|
||||
f"Timed out waiting for quit confirmation prompt.\n\nRendered tail:\n{rendered_tail}"
|
||||
)
|
||||
|
|
|
|||
87
tests/e2e/test_cli_tui_hooks.py
Normal file
87
tests/e2e/test_cli_tui_hooks.py
Normal file
|
|
@ -0,0 +1,87 @@
|
|||
from __future__ import annotations
|
||||
|
||||
import json
|
||||
import os
|
||||
from pathlib import Path
|
||||
import tomllib
|
||||
|
||||
import pexpect
|
||||
import pytest
|
||||
import tomli_w
|
||||
|
||||
from tests.e2e.common import (
|
||||
SpawnedVibeProcessFixture,
|
||||
poll_until,
|
||||
send_ctrl_c_until_quit_confirmation,
|
||||
wait_for_main_screen,
|
||||
wait_for_rendered_text,
|
||||
wait_for_request_count,
|
||||
)
|
||||
from tests.e2e.mock_server import StreamingMockServer
|
||||
|
||||
|
||||
def _enable_hooks(vibe_home: Path, invocation_path: Path) -> None:
|
||||
config_path = vibe_home / "config.toml"
|
||||
config = tomllib.loads(config_path.read_text(encoding="utf-8"))
|
||||
config["enable_experimental_hooks"] = True
|
||||
config_path.write_bytes(tomli_w.dumps(config).encode())
|
||||
|
||||
script = vibe_home / "_record_hook.py"
|
||||
script.write_text(
|
||||
"import json, sys\n"
|
||||
"from pathlib import Path\n"
|
||||
f"Path({str(invocation_path)!r}).write_text("
|
||||
"json.dumps(json.load(sys.stdin)), encoding='utf-8')\n",
|
||||
encoding="utf-8",
|
||||
)
|
||||
with (vibe_home / "hooks.toml").open("wb") as f:
|
||||
tomli_w.dump(
|
||||
{
|
||||
"hooks": [
|
||||
{
|
||||
"name": "record-invocation",
|
||||
"type": "post_agent_turn",
|
||||
"command": f"uv run python {script}",
|
||||
}
|
||||
]
|
||||
},
|
||||
f,
|
||||
)
|
||||
|
||||
|
||||
@pytest.mark.timeout(20)
|
||||
def test_spawn_cli_runs_configured_hook_after_turn(
|
||||
streaming_mock_server: StreamingMockServer,
|
||||
setup_e2e_env: None,
|
||||
e2e_workdir: Path,
|
||||
spawned_vibe_process: SpawnedVibeProcessFixture,
|
||||
) -> None:
|
||||
vibe_home = Path(os.environ["VIBE_HOME"])
|
||||
invocation_path = vibe_home / "hook-invocation.json"
|
||||
_enable_hooks(vibe_home, invocation_path)
|
||||
|
||||
with spawned_vibe_process(e2e_workdir) as (child, captured):
|
||||
wait_for_main_screen(child, timeout=15)
|
||||
child.send("Run the configured hook")
|
||||
child.send("\r")
|
||||
|
||||
wait_for_request_count(
|
||||
lambda: len(streaming_mock_server.requests), expected_count=1, timeout=10
|
||||
)
|
||||
wait_for_rendered_text(
|
||||
child, captured, needle="Hello from mock server", timeout=10
|
||||
)
|
||||
poll_until(
|
||||
invocation_path.is_file,
|
||||
timeout=10,
|
||||
message=f"Timed out waiting for hook output file: {invocation_path}",
|
||||
)
|
||||
|
||||
send_ctrl_c_until_quit_confirmation(child, captured, timeout=5)
|
||||
child.expect(pexpect.EOF, timeout=10)
|
||||
|
||||
assert len(streaming_mock_server.requests) == 1
|
||||
invocation = json.loads(invocation_path.read_text(encoding="utf-8"))
|
||||
assert invocation["hook_event_name"] == "post_agent_turn"
|
||||
assert isinstance(invocation["cwd"], str) and invocation["cwd"]
|
||||
assert isinstance(invocation["session_id"], str) and invocation["session_id"]
|
||||
|
|
@ -12,6 +12,7 @@ import pytest
|
|||
from tests.e2e.common import (
|
||||
SpawnedVibeProcessFixture,
|
||||
ansi_tolerant_pattern,
|
||||
send_ctrl_c_until_quit_confirmation,
|
||||
strip_ansi,
|
||||
wait_for_main_screen,
|
||||
wait_for_request_count,
|
||||
|
|
@ -103,7 +104,7 @@ def test_resumed_session_prints_only_fresh_token_usage_on_exit(
|
|||
request_count_getter=lambda: len(streaming_mock_server.requests),
|
||||
)
|
||||
|
||||
child.sendcontrol("c")
|
||||
send_ctrl_c_until_quit_confirmation(child, captured, timeout=5)
|
||||
child.expect(pexpect.EOF, timeout=10)
|
||||
|
||||
first_output = strip_ansi(captured.getvalue())
|
||||
|
|
@ -130,7 +131,7 @@ def test_resumed_session_prints_only_fresh_token_usage_on_exit(
|
|||
request_count_getter=lambda: len(streaming_mock_server.requests),
|
||||
)
|
||||
|
||||
resumed_child.sendcontrol("c")
|
||||
send_ctrl_c_until_quit_confirmation(resumed_child, resumed_captured, timeout=5)
|
||||
resumed_child.expect(pexpect.EOF, timeout=10)
|
||||
|
||||
second_output = strip_ansi(resumed_captured.getvalue())
|
||||
|
|
|
|||
|
|
@ -8,6 +8,7 @@ import pytest
|
|||
from tests.e2e.common import (
|
||||
SpawnedVibeProcessFixture,
|
||||
ansi_tolerant_pattern,
|
||||
send_ctrl_c_until_quit_confirmation,
|
||||
wait_for_main_screen,
|
||||
wait_for_request_count,
|
||||
)
|
||||
|
|
@ -31,7 +32,7 @@ def test_spawn_cli_to_send_and_receive_message(
|
|||
)
|
||||
child.expect(ansi_tolerant_pattern("Hello from mock server"), timeout=10)
|
||||
|
||||
child.sendcontrol("c")
|
||||
send_ctrl_c_until_quit_confirmation(child, captured, timeout=5)
|
||||
child.expect(pexpect.EOF, timeout=10)
|
||||
|
||||
output = captured.getvalue()
|
||||
|
|
|
|||
|
|
@ -7,6 +7,7 @@ import pytest
|
|||
|
||||
from tests.e2e.common import (
|
||||
SpawnedVibeProcessFixture,
|
||||
send_ctrl_c_until_quit_confirmation,
|
||||
wait_for_main_screen,
|
||||
wait_for_rendered_text,
|
||||
wait_for_request_count,
|
||||
|
|
@ -83,5 +84,5 @@ def test_spawn_cli_asks_bash_permission_and_shows_tool_output_after_approval(
|
|||
child.send("\r")
|
||||
wait_for_rendered_text(child, captured, needle=PREDICTABLE_OUTPUT, timeout=10)
|
||||
|
||||
child.sendcontrol("c")
|
||||
send_ctrl_c_until_quit_confirmation(child, captured, timeout=5)
|
||||
child.expect(pexpect.EOF, timeout=10)
|
||||
|
|
|
|||
Loading…
Add table
Add a link
Reference in a new issue