claw-code/tests/test_flush_transcript_cli.py

207 lines
7.4 KiB
Python

"""Tests for flush-transcript CLI parity with the #160/#165 lifecycle triplet (ROADMAP #166).
Verifies that session *creation* now accepts the same flag family as session
management (list/delete/load):
- --directory DIR (alternate storage location)
- --output-format {text,json} (structured output)
- --session-id ID (deterministic IDs for claw checkpointing)
Also verifies backward compat: default text output unchanged byte-for-byte.
"""
from __future__ import annotations
import json
import subprocess
import sys
from pathlib import Path
import pytest
sys.path.insert(0, str(Path(__file__).resolve().parent.parent))
_REPO_ROOT = Path(__file__).resolve().parent.parent
def _run_cli(*args: str) -> subprocess.CompletedProcess[str]:
return subprocess.run(
[sys.executable, '-m', 'src.main', *args],
capture_output=True, text=True, cwd=str(_REPO_ROOT),
)
class TestDirectoryFlag:
def test_flush_transcript_writes_to_custom_directory(self, tmp_path: Path) -> None:
result = _run_cli(
'flush-transcript', 'hello world',
'--directory', str(tmp_path),
)
assert result.returncode == 0, result.stderr
# Exactly one session file should exist in the directory
files = list(tmp_path.glob('*.json'))
assert len(files) == 1
# And the legacy text output points to that file
assert str(files[0]) in result.stdout
class TestSessionIdFlag:
def test_explicit_session_id_is_respected(self, tmp_path: Path) -> None:
result = _run_cli(
'flush-transcript', 'hello',
'--directory', str(tmp_path),
'--session-id', 'deterministic-id-42',
)
assert result.returncode == 0, result.stderr
expected_path = tmp_path / 'deterministic-id-42.json'
assert expected_path.exists(), (
f'session file not created at deterministic path: {expected_path}'
)
# And it should contain the ID we asked for
data = json.loads(expected_path.read_text())
assert data['session_id'] == 'deterministic-id-42'
def test_auto_session_id_when_flag_omitted(self, tmp_path: Path) -> None:
"""Without --session-id, engine still auto-generates a UUID (backward compat)."""
result = _run_cli(
'flush-transcript', 'hello',
'--directory', str(tmp_path),
)
assert result.returncode == 0
files = list(tmp_path.glob('*.json'))
assert len(files) == 1
# The filename (minus .json) should be a 32-char hex UUID
stem = files[0].stem
assert len(stem) == 32
assert all(c in '0123456789abcdef' for c in stem)
class TestOutputFormatFlag:
def test_json_mode_emits_structured_envelope(self, tmp_path: Path) -> None:
result = _run_cli(
'flush-transcript', 'hello',
'--directory', str(tmp_path),
'--session-id', 'beta',
'--output-format', 'json',
)
assert result.returncode == 0
data = json.loads(result.stdout)
assert data['session_id'] == 'beta'
assert data['flushed'] is True
assert data['path'].endswith('beta.json')
# messages_count and token counts should be present and typed
assert isinstance(data['messages_count'], int)
assert isinstance(data['input_tokens'], int)
assert isinstance(data['output_tokens'], int)
def test_text_mode_byte_identical_to_pre_166_output(self, tmp_path: Path) -> None:
"""Legacy text output must not change — claws may be parsing it."""
result = _run_cli(
'flush-transcript', 'hello',
'--directory', str(tmp_path),
)
assert result.returncode == 0
lines = result.stdout.strip().split('\n')
# Line 1: path ending in .json
assert lines[0].endswith('.json')
# Line 2: exact legacy format
assert lines[1] == 'flushed=True'
class TestBackwardCompat:
def test_no_flags_default_behaviour(self, tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> None:
"""Running with no flags still works (default dir, text mode, auto UUID)."""
import os
env = os.environ.copy()
env['PYTHONPATH'] = str(_REPO_ROOT)
result = subprocess.run(
[sys.executable, '-m', 'src.main', 'flush-transcript', 'hello'],
capture_output=True, text=True, cwd=str(tmp_path), env=env,
)
assert result.returncode == 0, result.stderr
# Default dir is `.port_sessions` in CWD
sessions_dir = tmp_path / '.port_sessions'
assert sessions_dir.exists()
assert len(list(sessions_dir.glob('*.json'))) == 1
class TestLifecycleIntegration:
"""#166's real value: the triplet + creation command are now a coherent family."""
def test_create_then_list_then_load_then_delete_roundtrip(
self, tmp_path: Path,
) -> None:
"""End-to-end: flush → list → load → delete, all via the same --directory."""
# 1. Create
create_result = _run_cli(
'flush-transcript', 'roundtrip test',
'--directory', str(tmp_path),
'--session-id', 'rt-session',
'--output-format', 'json',
)
assert create_result.returncode == 0
assert json.loads(create_result.stdout)['session_id'] == 'rt-session'
# 2. List
list_result = _run_cli(
'list-sessions',
'--directory', str(tmp_path),
'--output-format', 'json',
)
assert list_result.returncode == 0
list_data = json.loads(list_result.stdout)
assert 'rt-session' in list_data['sessions']
# 3. Load
load_result = _run_cli(
'load-session', 'rt-session',
'--directory', str(tmp_path),
'--output-format', 'json',
)
assert load_result.returncode == 0
assert json.loads(load_result.stdout)['loaded'] is True
# 4. Delete
delete_result = _run_cli(
'delete-session', 'rt-session',
'--directory', str(tmp_path),
'--output-format', 'json',
)
assert delete_result.returncode == 0
# 5. Verify gone
verify_result = _run_cli(
'load-session', 'rt-session',
'--directory', str(tmp_path),
'--output-format', 'json',
)
assert verify_result.returncode == 1
assert json.loads(verify_result.stdout)['error']['kind'] == 'session_not_found'
class TestFullFamilyParity:
"""All four session-lifecycle CLI commands accept the same core flag pair.
This is the #166 acceptance test: flush-transcript joins the family.
"""
@pytest.mark.parametrize(
'command',
['list-sessions', 'delete-session', 'load-session', 'flush-transcript'],
)
def test_all_four_accept_directory_flag(self, command: str) -> None:
help_text = _run_cli(command, '--help').stdout
assert '--directory' in help_text, (
f'{command} missing --directory flag (#166 parity gap)'
)
@pytest.mark.parametrize(
'command',
['list-sessions', 'delete-session', 'load-session', 'flush-transcript'],
)
def test_all_four_accept_output_format_flag(self, command: str) -> None:
help_text = _run_cli(command, '--help').stdout
assert '--output-format' in help_text, (
f'{command} missing --output-format flag (#166 parity gap)'
)