claude-os/tests/test_session_parser.py

261 lines
8.1 KiB
Python
Raw Normal View History

"""
Tests for session parser and insight extractor.
"""
import json
import pytest
from pathlib import Path
from app.core.session_parser import (
SessionParser,
get_project_sessions_dir,
list_session_files,
Message,
ToolCall,
FileChange,
SessionData
)
@pytest.fixture
def sample_session_file(tmp_path):
"""Create a sample session file for testing."""
session_file = tmp_path / "test-session.jsonl"
entries = [
{
"type": "summary",
"summary": "Test session summary",
"leafUuid": "abc123"
},
{
"type": "user",
"uuid": "msg-001",
"parentUuid": None,
"timestamp": "2025-12-11T10:00:00Z",
"cwd": "/Users/test/Projects/myapp",
"gitBranch": "main",
"sessionId": "test-session",
"message": {
"role": "user",
"content": "Hello, can you help me with testing?"
}
},
{
"type": "assistant",
"uuid": "msg-002",
"parentUuid": "msg-001",
"timestamp": "2025-12-11T10:00:05Z",
"message": {
"role": "assistant",
"content": [
{"type": "text", "text": "Of course! I can help you with testing."}
]
}
},
{
"type": "user",
"uuid": "msg-003",
"parentUuid": "msg-002",
"timestamp": "2025-12-11T10:00:10Z",
"message": {
"role": "user",
"content": "Can you write a test for me?"
}
},
{
"type": "assistant",
"uuid": "msg-004",
"parentUuid": "msg-003",
"timestamp": "2025-12-11T10:00:15Z",
"message": {
"role": "assistant",
"content": [
{"type": "text", "text": "I'll write a test for you."},
{
"type": "tool_use",
"id": "tool-001",
"name": "Write",
"input": {
"file_path": "/Users/test/Projects/myapp/test.py",
"content": "def test_example():\n pass"
}
}
]
}
},
{
"type": "file-history-snapshot",
"messageId": "msg-004",
"snapshot": {
"messageId": "msg-004",
"timestamp": "2025-12-11T10:00:20Z",
"trackedFileBackups": {
"/Users/test/Projects/myapp/test.py": {"content": "def test_example():\n pass"}
}
}
}
]
with open(session_file, 'w') as f:
for entry in entries:
f.write(json.dumps(entry) + '\n')
return session_file
def test_session_parser_init(sample_session_file):
"""Test SessionParser initialization."""
parser = SessionParser(str(sample_session_file))
assert parser.path == sample_session_file
assert parser.session_id == "test-session"
def test_session_parser_parse(sample_session_file):
"""Test parsing a session file."""
parser = SessionParser(str(sample_session_file))
session_data = parser.parse()
# Check basic metadata
assert session_data.session_id == "test-session"
assert session_data.cwd == "/Users/test/Projects/myapp"
assert session_data.git_branch == "main"
assert session_data.total_entries == 6
# Check messages (2 user + 2 assistant = 4 messages)
assert len(session_data.messages) == 4
assert session_data.messages[0].role == "user"
assert "help me with testing" in session_data.messages[0].content
assert session_data.messages[1].role == "assistant"
assert "help you with testing" in session_data.messages[1].content
# Check tool calls
assert len(session_data.tool_calls) == 1
assert session_data.tool_calls[0].tool_name == "Write"
assert session_data.tool_calls[0].input_data["file_path"] == "/Users/test/Projects/myapp/test.py"
# Check file changes
assert len(session_data.file_changes) == 1
assert session_data.file_changes[0].file_path == "/Users/test/Projects/myapp/test.py"
def test_session_parser_get_conversation(sample_session_file):
"""Test getting conversation messages."""
parser = SessionParser(str(sample_session_file))
messages = parser.get_conversation()
assert len(messages) == 4 # 2 user + 2 assistant messages
assert all(isinstance(msg, Message) for msg in messages)
assert messages[0].role == "user"
assert messages[1].role == "assistant"
def test_session_parser_get_summary(sample_session_file):
"""Test generating summary for extraction."""
parser = SessionParser(str(sample_session_file))
summary = parser.get_summary_for_extraction()
assert "test-session" in summary
assert "/Users/test/Projects/myapp" in summary
assert "main" in summary
assert "help me with testing" in summary
assert "Write" in summary
def test_get_project_sessions_dir():
"""Test encoding project path to sessions directory."""
project_path = "/Users/test/Projects/myapp"
sessions_dir = get_project_sessions_dir(project_path)
# Leading hyphen is preserved from the path encoding
expected = Path.home() / ".claude" / "projects" / "-Users-test-Projects-myapp"
assert sessions_dir == expected
def test_list_session_files_empty_dir(tmp_path):
"""Test listing sessions when directory doesn't exist."""
fake_project = str(tmp_path / "nonexistent")
sessions = list_session_files(fake_project)
assert sessions == []
def test_session_parser_invalid_file():
"""Test parser with non-existent file."""
with pytest.raises(FileNotFoundError):
parser = SessionParser("/path/to/nonexistent/file.jsonl")
def test_session_parser_malformed_json(tmp_path):
"""Test parser handles malformed JSON gracefully."""
session_file = tmp_path / "malformed.jsonl"
with open(session_file, 'w') as f:
f.write('{"type": "user", "content": "valid"}\n')
f.write('this is not json\n')
f.write('{"type": "assistant", "content": "also valid"}\n')
parser = SessionParser(str(session_file))
session_data = parser.parse()
# Should parse the valid lines, skip the malformed one
assert session_data.total_entries == 2
def test_session_data_defaults():
"""Test SessionData dataclass defaults."""
data = SessionData(
session_id="test",
session_path="/path/to/session.jsonl"
)
assert data.messages == []
assert data.tool_calls == []
assert data.file_changes == []
assert data.start_time is None
assert data.end_time is None
assert data.total_entries == 0
@pytest.mark.asyncio
async def test_insight_extractor_import():
"""Test that InsightExtractor can be imported."""
from app.core.insight_extractor import InsightExtractor, Insight
# Just verify imports work
assert InsightExtractor is not None
assert Insight is not None
def test_session_parser_truncation(tmp_path):
"""Test that very long sessions are truncated in summary."""
session_file = tmp_path / "long-session.jsonl"
# Create a very long message
long_content = "A" * 2000
entries = [
{
"type": "user",
"uuid": "msg-001",
"parentUuid": None,
"timestamp": "2025-12-11T10:00:00Z",
"cwd": "/test",
"sessionId": "long-session",
"message": {
"role": "user",
"content": long_content
}
}
]
with open(session_file, 'w') as f:
for entry in entries:
f.write(json.dumps(entry) + '\n')
parser = SessionParser(str(session_file))
summary = parser.get_summary_for_extraction(max_tokens=100)
# Summary should be shorter than the original content
# (header adds overhead, so just verify truncation occurred)
assert len(summary) < len(long_content)
assert "[... truncated ...]" in summary