- test_oauth_identity_block: verify identity block is included in API requests even when system=None (covers fix in3f2684d) - test_mem0_extract_facts: verify extract_facts passes thinking_budget=0 to provider.chat() (covers fix in76d5a73) - test_session_audit_log: verify save() creates append-only audit log with markers and message preservation (covers feat in2ab6494) Co-Authored-By: Claude Opus 4.6 <noreply@anthropic.com>
138 lines
4.6 KiB
Python
138 lines
4.6 KiB
Python
"""Test SessionManager audit log functionality."""
|
|
|
|
import json
|
|
|
|
import pytest
|
|
|
|
from nanobot.session.manager import Session, SessionManager
|
|
|
|
|
|
@pytest.fixture
|
|
def session_manager(tmp_path):
|
|
return SessionManager(workspace=tmp_path)
|
|
|
|
|
|
@pytest.fixture
|
|
def session():
|
|
s = Session(key="telegram:12345")
|
|
s.add_message("user", "Hello")
|
|
s.add_message("assistant", "Hi there!")
|
|
return s
|
|
|
|
|
|
def test_save_creates_audit_file(session_manager, session):
|
|
"""SessionManager.save() should create a monthly audit log file."""
|
|
session_manager.save(session)
|
|
|
|
audit_files = list(session_manager.sessions_dir.glob("*.audit.*.jsonl"))
|
|
assert len(audit_files) == 1
|
|
assert "telegram_12345.audit." in audit_files[0].name
|
|
|
|
|
|
def test_audit_file_contains_save_marker(session_manager, session):
|
|
"""Audit log should start with a save_marker line containing metadata."""
|
|
session_manager.save(session)
|
|
|
|
audit_files = list(session_manager.sessions_dir.glob("*.audit.*.jsonl"))
|
|
lines = audit_files[0].read_text().strip().split("\n")
|
|
|
|
marker = json.loads(lines[0])
|
|
assert marker["_type"] == "save_marker"
|
|
assert marker["message_count"] == 2
|
|
assert "timestamp" in marker
|
|
|
|
|
|
def test_audit_file_contains_all_messages(session_manager, session):
|
|
"""Audit log should contain all session messages after the save marker."""
|
|
session_manager.save(session)
|
|
|
|
audit_files = list(session_manager.sessions_dir.glob("*.audit.*.jsonl"))
|
|
lines = audit_files[0].read_text().strip().split("\n")
|
|
|
|
# Line 0 = save_marker, lines 1-2 = messages
|
|
assert len(lines) == 3
|
|
msg1 = json.loads(lines[1])
|
|
msg2 = json.loads(lines[2])
|
|
assert msg1["role"] == "user"
|
|
assert msg1["content"] == "Hello"
|
|
assert msg2["role"] == "assistant"
|
|
assert msg2["content"] == "Hi there!"
|
|
|
|
|
|
def test_audit_file_is_append_only(session_manager, session):
|
|
"""Multiple saves should append to the same audit file, not overwrite."""
|
|
session_manager.save(session)
|
|
|
|
# Add another message and save again
|
|
session.add_message("user", "How are you?")
|
|
session_manager.save(session)
|
|
|
|
audit_files = list(session_manager.sessions_dir.glob("*.audit.*.jsonl"))
|
|
assert len(audit_files) == 1 # Same file
|
|
|
|
lines = audit_files[0].read_text().strip().split("\n")
|
|
|
|
# First save: 1 marker + 2 messages = 3 lines
|
|
# Second save: 1 marker + 3 messages = 4 lines
|
|
# Total: 7 lines
|
|
assert len(lines) == 7
|
|
|
|
# Both save markers present
|
|
markers = [json.loads(l) for l in lines if json.loads(l).get("_type") == "save_marker"]
|
|
assert len(markers) == 2
|
|
assert markers[0]["message_count"] == 2
|
|
assert markers[1]["message_count"] == 3
|
|
|
|
|
|
def test_audit_preserves_message_fields(session_manager):
|
|
"""Audit log should preserve all message fields including reasoning_content."""
|
|
session = Session(key="test:preserve")
|
|
session.add_raw_message({
|
|
"role": "assistant",
|
|
"content": "thinking response",
|
|
"reasoning_content": [{"type": "thinking", "thinking": "deep thoughts"}],
|
|
"timestamp": "2026-03-22T12:00:00",
|
|
})
|
|
|
|
session_manager.save(session)
|
|
|
|
audit_files = list(session_manager.sessions_dir.glob("*.audit.*.jsonl"))
|
|
lines = audit_files[0].read_text().strip().split("\n")
|
|
|
|
msg = json.loads(lines[1])
|
|
assert msg["reasoning_content"] == [{"type": "thinking", "thinking": "deep thoughts"}]
|
|
|
|
|
|
def test_audit_failure_does_not_break_save(session_manager, session, tmp_path):
|
|
"""If audit logging fails, the main session save should still succeed.
|
|
|
|
_append_audit has its own try/except, so internal failures are caught.
|
|
We simulate a realistic failure by making the sessions dir read-only
|
|
for audit file creation.
|
|
"""
|
|
# First save works (creates both session file and audit file)
|
|
session_manager.save(session)
|
|
|
|
path = session_manager._get_session_path(session.key)
|
|
assert path.exists()
|
|
|
|
# Remove audit files and make a blocking file at the audit path
|
|
# so the next audit open("a") fails
|
|
for af in session_manager.sessions_dir.glob("*.audit.*.jsonl"):
|
|
af.unlink()
|
|
|
|
# Create a directory where the audit file should be — open() will fail
|
|
from datetime import datetime
|
|
now = datetime.now()
|
|
bad_path = session_manager.sessions_dir / f"telegram_12345.audit.{now:%Y-%m}.jsonl"
|
|
bad_path.mkdir()
|
|
|
|
# Second save should succeed despite audit failure
|
|
session.add_message("user", "another message")
|
|
session_manager.save(session)
|
|
|
|
# Session file should still be written correctly
|
|
with open(path) as f:
|
|
first_line = json.loads(f.readline())
|
|
assert first_line["_type"] == "metadata"
|