Files
EvoScientist/tests/test_sessions.py
T
X-iZhang 35135ee79c feat(tests): add additional smoke tests for email, signal, and QQ channels
feat(tests): enhance relative time formatting tests for minutes, hours, days, and months
chore(docs): update contributing guidelines for testing commands
2026-02-25 10:05:29 +00:00

302 lines
11 KiB
Python

"""Tests for EvoScientist.sessions — thread CRUD, ID generation, helpers."""
import json
import os
import tempfile
import unittest
from unittest.mock import patch
from EvoScientist.sessions import (
AGENT_NAME,
_format_relative_time,
delete_thread,
find_similar_threads,
generate_thread_id,
get_db_path,
get_most_recent,
get_thread_metadata,
list_threads,
thread_exists,
)
from tests.conftest import run_async as _run
class TestGenerateThreadId(unittest.TestCase):
def test_length(self):
tid = generate_thread_id()
self.assertEqual(len(tid), 8)
def test_hex(self):
tid = generate_thread_id()
int(tid, 16) # Should not raise
def test_uniqueness(self):
ids = {generate_thread_id() for _ in range(100)}
self.assertEqual(len(ids), 100)
class TestGetDbPath(unittest.TestCase):
def test_uses_config_dir(self):
path = get_db_path()
self.assertTrue(str(path).endswith("sessions.db"))
self.assertIn(".config", str(path))
self.assertIn("evoscientist", str(path))
class TestFormatRelativeTime(unittest.TestCase):
def test_none(self):
self.assertEqual(_format_relative_time(None), "")
def test_invalid(self):
self.assertEqual(_format_relative_time("not-a-date"), "")
def test_recent(self):
from datetime import datetime, timezone
now = datetime.now(timezone.utc).isoformat()
result = _format_relative_time(now)
self.assertIn("just now", result)
def test_minutes(self):
from datetime import datetime, timedelta, timezone
ts = (datetime.now(timezone.utc) - timedelta(minutes=5)).isoformat()
result = _format_relative_time(ts)
self.assertIn("min ago", result)
def test_hours(self):
from datetime import datetime, timedelta, timezone
ts = (datetime.now(timezone.utc) - timedelta(hours=2)).isoformat()
result = _format_relative_time(ts)
self.assertIn("hour", result)
def test_days(self):
from datetime import datetime, timedelta, timezone
ts = (datetime.now(timezone.utc) - timedelta(days=3)).isoformat()
result = _format_relative_time(ts)
self.assertIn("day", result)
def test_months(self):
from datetime import datetime, timedelta, timezone
ts = (datetime.now(timezone.utc) - timedelta(days=65)).isoformat()
result = _format_relative_time(ts)
self.assertIn("month", result)
class TestThreadFunctions(unittest.TestCase):
"""Tests using a real temporary SQLite database."""
@classmethod
def setUpClass(cls):
"""Create a temp DB and populate with test data."""
cls._tmpdir = tempfile.mkdtemp()
cls._db_path = os.path.join(cls._tmpdir, "test_sessions.db")
async def _setup():
import aiosqlite
async with aiosqlite.connect(cls._db_path) as conn:
# Create tables matching LangGraph checkpoint schema
await conn.execute("""
CREATE TABLE IF NOT EXISTS checkpoints (
thread_id TEXT NOT NULL,
checkpoint_ns TEXT NOT NULL DEFAULT '',
checkpoint_id TEXT NOT NULL,
parent_checkpoint_id TEXT,
type TEXT,
checkpoint BLOB,
metadata TEXT NOT NULL DEFAULT '{}',
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id)
)
""")
await conn.execute("""
CREATE TABLE IF NOT EXISTS writes (
thread_id TEXT NOT NULL,
checkpoint_ns TEXT NOT NULL DEFAULT '',
checkpoint_id TEXT NOT NULL,
task_id TEXT NOT NULL,
idx INTEGER NOT NULL,
channel TEXT NOT NULL,
type TEXT,
blob BLOB,
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
)
""")
# Insert test checkpoints
for i, tid in enumerate(["abc12345", "abc12399", "def00001"]):
meta = json.dumps({
"agent_name": AGENT_NAME,
"updated_at": f"2025-01-{15 + i}T10:00:00+00:00",
"workspace_dir": f"/tmp/ws_{tid}",
"model": "claude-sonnet-4-5",
})
await conn.execute(
"INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, metadata) VALUES (?, '', ?, ?)",
(tid, f"cp_{i}", meta),
)
# Insert a non-EvoScientist checkpoint (should be filtered)
other_meta = json.dumps({
"agent_name": "OtherAgent",
"updated_at": "2025-01-20T10:00:00+00:00",
})
await conn.execute(
"INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, metadata) VALUES (?, '', ?, ?)",
("zzz99999", "cp_other", other_meta),
)
await conn.commit()
_run(_setup())
# Patch get_db_path to point to our temp DB
cls._patcher = patch(
"EvoScientist.sessions.get_db_path",
return_value=type("P", (), {"__str__": lambda s: cls._db_path, "__fspath__": lambda s: cls._db_path})(),
)
cls._patcher.start()
@classmethod
def tearDownClass(cls):
cls._patcher.stop()
try:
os.unlink(cls._db_path)
os.rmdir(cls._tmpdir)
except OSError:
pass
def test_list_threads(self):
threads = _run(list_threads(limit=10))
# Should only contain EvoScientist threads
self.assertEqual(len(threads), 3)
# Most recent first
self.assertEqual(threads[0]["thread_id"], "def00001")
def test_list_threads_with_message_count(self):
threads = _run(list_threads(limit=10, include_message_count=True))
self.assertIn("message_count", threads[0])
def test_thread_exists_true(self):
self.assertTrue(_run(thread_exists("abc12345")))
def test_thread_exists_false(self):
self.assertFalse(_run(thread_exists("nonexist")))
def test_find_similar(self):
similar = _run(find_similar_threads("abc1"))
self.assertEqual(len(similar), 2)
self.assertIn("abc12345", similar)
self.assertIn("abc12399", similar)
def test_find_similar_no_match(self):
similar = _run(find_similar_threads("xyz"))
self.assertEqual(len(similar), 0)
def test_get_most_recent(self):
recent = _run(get_most_recent())
self.assertIsNotNone(recent)
self.assertEqual(recent, "def00001")
def test_get_thread_metadata(self):
meta = _run(get_thread_metadata("abc12345"))
self.assertIsNotNone(meta)
self.assertEqual(meta["workspace_dir"], "/tmp/ws_abc12345")
self.assertEqual(meta["model"], "claude-sonnet-4-5")
def test_get_thread_metadata_missing(self):
meta = _run(get_thread_metadata("nonexist"))
self.assertIsNone(meta)
def test_delete_thread(self):
# Insert a thread to delete
async def _insert():
import aiosqlite
async with aiosqlite.connect(self._db_path) as conn:
meta = json.dumps({"agent_name": AGENT_NAME, "updated_at": "2025-01-01T00:00:00+00:00"})
await conn.execute(
"INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, metadata) VALUES (?, '', ?, ?)",
("todelete", "cp_del", meta),
)
await conn.commit()
_run(_insert())
self.assertTrue(_run(thread_exists("todelete")))
self.assertTrue(_run(delete_thread("todelete")))
self.assertFalse(_run(thread_exists("todelete")))
def test_delete_nonexistent(self):
self.assertFalse(_run(delete_thread("nope1234")))
# -- Agent isolation: OtherAgent data should never be visible --
def test_thread_exists_ignores_other_agent(self):
self.assertFalse(_run(thread_exists("zzz99999")))
def test_find_similar_ignores_other_agent(self):
similar = _run(find_similar_threads("zzz"))
self.assertEqual(len(similar), 0)
def test_get_metadata_ignores_other_agent(self):
meta = _run(get_thread_metadata("zzz99999"))
self.assertIsNone(meta)
def test_delete_ignores_other_agent(self):
# Should not delete OtherAgent's data
self.assertFalse(_run(delete_thread("zzz99999")))
def test_delete_thread_preserves_other_agent_writes(self):
"""Deleting a shared thread_id must only remove writes linked to
EvoScientist checkpoints, leaving OtherAgent's writes intact."""
shared_tid = "shared01"
async def _insert():
import aiosqlite
async with aiosqlite.connect(self._db_path) as conn:
# EvoScientist checkpoint + write
evo_meta = json.dumps({"agent_name": AGENT_NAME, "updated_at": "2025-02-01T00:00:00+00:00"})
await conn.execute(
"INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, metadata) VALUES (?, '', ?, ?)",
(shared_tid, "cp_evo_shared", evo_meta),
)
await conn.execute(
"INSERT INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob) "
"VALUES (?, '', ?, 't1', 0, 'ch', 'str', X'AA')",
(shared_tid, "cp_evo_shared"),
)
# OtherAgent checkpoint + write on the SAME thread_id
other_meta = json.dumps({"agent_name": "OtherAgent", "updated_at": "2025-02-01T00:00:00+00:00"})
await conn.execute(
"INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id, metadata) VALUES (?, '', ?, ?)",
(shared_tid, "cp_other_shared", other_meta),
)
await conn.execute(
"INSERT INTO writes (thread_id, checkpoint_ns, checkpoint_id, task_id, idx, channel, type, blob) "
"VALUES (?, '', ?, 't2', 0, 'ch', 'str', X'BB')",
(shared_tid, "cp_other_shared"),
)
await conn.commit()
_run(_insert())
# Delete — should only affect EvoScientist's data
_run(delete_thread(shared_tid))
# Verify OtherAgent's writes survive
async def _check():
import aiosqlite
async with aiosqlite.connect(self._db_path) as conn:
async with conn.execute(
"SELECT checkpoint_id FROM writes WHERE thread_id = ?", (shared_tid,)
) as cur:
rows = await cur.fetchall()
return [r[0] for r in rows]
remaining = _run(_check())
self.assertIn("cp_other_shared", remaining)
self.assertNotIn("cp_evo_shared", remaining)
if __name__ == "__main__":
unittest.main()