mirror of
https://github.com/ultraworkers/claw-code.git
synced 2026-04-25 13:44:06 +08:00
207 lines
7.4 KiB
Python
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)'
|
|
)
|