from __future__ import annotations from collections.abc import Callable import io import json import os from pathlib import Path import re import time import pexpect import pytest from tests.e2e.common import ( SpawnedVibeProcessFixture, ansi_tolerant_pattern, poll_until, send_ctrl_c_until_quit_confirmation, strip_ansi, wait_for_main_screen, wait_for_request_count, ) from tests.e2e.mock_server import StreamingMockServer from vibe.core.utils.io import read_safe def _usage_by_run_factory( request_index: int, _payload: object ) -> list[dict[str, object]]: return [ StreamingMockServer.build_chunk( created=123, delta={"role": "assistant", "content": f"Reply {request_index + 1}"}, finish_reason=None, ), StreamingMockServer.build_chunk( created=124, delta={}, finish_reason="stop", usage=( {"prompt_tokens": 11, "completion_tokens": 7} if request_index == 0 else {"prompt_tokens": 2, "completion_tokens": 1} ), ), ] def _saved_session_has_usage( vibe_home: Path, expected_prompt_tokens: int, expected_completion_tokens: int ) -> bool: session_log_dir = vibe_home / "logs" / "session" if not session_log_dir.exists(): return False for metadata_path in session_log_dir.glob("session_*/meta.json"): try: metadata = json.loads(read_safe(metadata_path).text) except (OSError, json.JSONDecodeError): continue stats = metadata.get("stats", {}) if not isinstance(stats, dict): continue if ( stats.get("session_prompt_tokens") == expected_prompt_tokens and stats.get("session_completion_tokens") == expected_completion_tokens ): return True return False def _wait_for_saved_session_usage( expected_prompt_tokens: int, expected_completion_tokens: int ) -> None: vibe_home = Path(os.environ["VIBE_HOME"]) poll_until( lambda: _saved_session_has_usage( vibe_home, expected_prompt_tokens, expected_completion_tokens ), timeout=10, message=( "Timed out waiting for saved session usage " f"input={expected_prompt_tokens} output={expected_completion_tokens}." ), ) def _finish_turn( child: pexpect.spawn, captured: io.StringIO, expected_reply: str, expected_request_count: int, request_count_getter: Callable[[], int], ) -> None: wait_for_request_count( request_count_getter, expected_count=expected_request_count, timeout=10 ) child.expect(ansi_tolerant_pattern(expected_reply), timeout=10) start = time.monotonic() last_change = start last_size = len(captured.getvalue()) while time.monotonic() - start < 5: try: child.expect(r"\S", timeout=0.05) except pexpect.TIMEOUT: pass current_size = len(captured.getvalue()) if current_size != last_size: last_size = current_size last_change = time.monotonic() continue if time.monotonic() - last_change >= 0.3: return rendered_tail = strip_ansi(captured.getvalue())[-1200:] raise AssertionError( f"Timed out waiting for the turn to finish.\n\nRendered tail:\n{rendered_tail}" ) @pytest.mark.timeout(30) @pytest.mark.parametrize( "streaming_mock_server", [pytest.param(_usage_by_run_factory, id="fresh-usage-after-resume")], indirect=True, ) def test_resumed_session_prints_only_fresh_token_usage_on_exit( streaming_mock_server: StreamingMockServer, setup_e2e_env: None, e2e_workdir: Path, spawned_vibe_process: SpawnedVibeProcessFixture, ) -> None: with spawned_vibe_process(e2e_workdir) as (child, captured): wait_for_main_screen(child, timeout=15) child.send("First run") child.send("\r") _finish_turn( child, captured, expected_reply="Reply 1", expected_request_count=1, request_count_getter=lambda: len(streaming_mock_server.requests), ) _wait_for_saved_session_usage( expected_prompt_tokens=11, expected_completion_tokens=7 ) send_ctrl_c_until_quit_confirmation(child, captured, timeout=5) child.expect(pexpect.EOF, timeout=10) first_output = strip_ansi(captured.getvalue()) resume_match = re.search(r"Or: vibe --resume ([0-9a-f-]+)", first_output) assert resume_match is not None session_id = resume_match.group(1) assert ( "Total tokens used this session: input=11 output=7 (total=18)" in first_output ) with spawned_vibe_process(e2e_workdir, extra_args=["--resume", session_id]) as ( resumed_child, resumed_captured, ): wait_for_main_screen(resumed_child, timeout=15) resumed_child.send("Second run") resumed_child.send("\r") _finish_turn( resumed_child, resumed_captured, expected_reply="Reply 2", expected_request_count=2, request_count_getter=lambda: len(streaming_mock_server.requests), ) _wait_for_saved_session_usage( expected_prompt_tokens=2, expected_completion_tokens=1 ) 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()) assert "Total tokens used this session: input=2 output=1 (total=3)" in second_output