feat: prepare EvoScientist 0.3.0
Docker / build (push) Has been cancelled
Build / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled
Docker / build (push) Has been cancelled
Build / build (push) Has been cancelled
Lint / ruff (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.11) (push) Has been cancelled
Test / pytest (ubuntu-latest, 3.12) (push) Has been cancelled
Test / pytest (windows-latest, 3.11) (push) Has been cancelled
Test / pytest (windows-latest, 3.12) (push) Has been cancelled
Add bounded document ingestion, controlled web search, recoverable session support, subagent timeouts, and the native sandbox runtime contract. Unify package versioning and add release-focused regression coverage.
This commit is contained in:
@@ -22,13 +22,19 @@ from EvoScientist.sessions import (
|
||||
delete_thread,
|
||||
find_similar_threads,
|
||||
generate_thread_id,
|
||||
get_aggregated_storage_stats,
|
||||
get_db_path,
|
||||
get_most_recent,
|
||||
get_thread_messages,
|
||||
get_thread_metadata,
|
||||
list_all_session_db_paths,
|
||||
list_all_thread_ids,
|
||||
list_threads,
|
||||
prune_all_stale_threads,
|
||||
prune_thread_history,
|
||||
resolve_thread_id_prefix,
|
||||
thread_exists,
|
||||
vacuum_db,
|
||||
)
|
||||
|
||||
|
||||
@@ -2919,5 +2925,172 @@ class TestRestoreWebuiThreadsToGlobalStore(unittest.IsolatedAsyncioTestCase):
|
||||
assert restore_called, "_restore_webui_threads_to_global_store must be called"
|
||||
|
||||
|
||||
def _uuid6_from_unix(ts_unix: float) -> str:
|
||||
"""Build a UUIDv6 (time-ordered checkpoint id) from a unix timestamp.
|
||||
|
||||
Production checkpoint ids are UUIDv6, so lexicographic order matches
|
||||
insertion order and the timestamp is recoverable from the id itself.
|
||||
"""
|
||||
greg = int((ts_unix + 12219292800) * 10_000_000) & ((1 << 60) - 1)
|
||||
high48, low12 = greg >> 12, greg & 0xFFF
|
||||
rand = uuid.uuid4().int & ((1 << 62) - 1)
|
||||
value = (high48 << 80) | (0x6 << 76) | (low12 << 64) | (0b10 << 62) | rand
|
||||
return str(uuid.UUID(int=value))
|
||||
|
||||
|
||||
class TestPruneFunctions(unittest.IsolatedAsyncioTestCase):
|
||||
"""Tests for the prune/vacuum API used by the gateway timer and admin routes."""
|
||||
|
||||
async def asyncSetUp(self):
|
||||
import time
|
||||
|
||||
import aiosqlite
|
||||
|
||||
self._tmpdir = tempfile.mkdtemp()
|
||||
self.db_path = os.path.join(self._tmpdir, "prune_test.db")
|
||||
now = time.time()
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as conn:
|
||||
await conn.execute("""
|
||||
CREATE TABLE 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 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,
|
||||
value BLOB,
|
||||
PRIMARY KEY (thread_id, checkpoint_ns, checkpoint_id, task_id, idx)
|
||||
)
|
||||
""")
|
||||
await self._insert_thread(conn, "old_thread", 5, now - 10 * 86400)
|
||||
await self._insert_thread(conn, "new_thread", 3, now - 60)
|
||||
await self._insert_thread(
|
||||
conn, "other", 4, now - 10 * 86400, agent="OtherAgent"
|
||||
)
|
||||
await conn.commit()
|
||||
|
||||
async def asyncTearDown(self):
|
||||
try:
|
||||
os.unlink(self.db_path)
|
||||
os.rmdir(self._tmpdir)
|
||||
except OSError:
|
||||
pass
|
||||
|
||||
async def _insert_thread(self, conn, tid, count, ts_base, agent=AGENT_NAME):
|
||||
serde = JsonPlusSerializer()
|
||||
ctype, cblob = serde.dumps_typed(
|
||||
{"channel_values": {"messages": [HumanMessage(content=f"seed-{tid}")]}}
|
||||
)
|
||||
prev = None
|
||||
for i in range(count):
|
||||
cid = _uuid6_from_unix(ts_base + i)
|
||||
await conn.execute(
|
||||
"INSERT INTO checkpoints (thread_id, checkpoint_ns, checkpoint_id,"
|
||||
" parent_checkpoint_id, type, checkpoint, metadata)"
|
||||
" VALUES (?, '', ?, ?, ?, ?, ?)",
|
||||
(tid, cid, prev, ctype, cblob, json.dumps({"agent_name": agent})),
|
||||
)
|
||||
await conn.execute(
|
||||
"INSERT INTO writes (thread_id, checkpoint_ns, checkpoint_id,"
|
||||
" task_id, idx, channel, type, value)"
|
||||
" VALUES (?, '', ?, 'task', 0, 'ch', 'str', ?)",
|
||||
(tid, cid, b"x"),
|
||||
)
|
||||
prev = cid
|
||||
|
||||
async def _count(self, tid, table="checkpoints"):
|
||||
import aiosqlite
|
||||
|
||||
async with aiosqlite.connect(self.db_path) as conn:
|
||||
async with conn.execute(
|
||||
f"SELECT COUNT(*) FROM {table} WHERE thread_id = ?", (tid,)
|
||||
) as cur:
|
||||
return (await cur.fetchone())[0]
|
||||
|
||||
async def test_prune_thread_history(self):
|
||||
result = await prune_thread_history(
|
||||
"old_thread", keep_last=2, db_path=self.db_path
|
||||
)
|
||||
# keep_last=2 anchors + 1 snapshot-seed ancestor preserved
|
||||
assert result == {"deleted_checkpoints": 2, "deleted_writes": 2}
|
||||
assert await self._count("old_thread") == 3
|
||||
assert await self._count("old_thread", "writes") == 3
|
||||
|
||||
async def test_prune_thread_history_other_agent_untouched(self):
|
||||
result = await prune_thread_history("other", keep_last=1, db_path=self.db_path)
|
||||
assert result == {"deleted_checkpoints": 0, "deleted_writes": 0}
|
||||
assert await self._count("other") == 4
|
||||
|
||||
async def test_prune_all_stale_threads(self):
|
||||
result = await prune_all_stale_threads(
|
||||
max_age_hours=24, keep_last=2, db_path=self.db_path
|
||||
)
|
||||
assert result["databases_processed"] == 1
|
||||
assert result["threads_pruned"] == 1
|
||||
assert result["total_deleted_checkpoints"] == 2
|
||||
assert result["total_deleted_writes"] == 2
|
||||
# fresh thread and foreign-agent thread untouched
|
||||
assert await self._count("new_thread") == 3
|
||||
assert await self._count("other") == 4
|
||||
|
||||
async def test_prune_all_stale_threads_none_stale(self):
|
||||
result = await prune_all_stale_threads(
|
||||
max_age_hours=24 * 365, keep_last=2, db_path=self.db_path
|
||||
)
|
||||
assert result["threads_pruned"] == 0
|
||||
assert result["total_deleted_checkpoints"] == 0
|
||||
assert await self._count("old_thread") == 5
|
||||
|
||||
async def test_list_all_thread_ids(self):
|
||||
ids = await list_all_thread_ids(db_path=self.db_path)
|
||||
assert sorted(ids) == ["new_thread", "old_thread"]
|
||||
|
||||
async def test_vacuum_db(self):
|
||||
result = await vacuum_db(db_path=self.db_path)
|
||||
assert result["size_after_bytes"] > 0
|
||||
assert result["size_before_bytes"] >= result["size_after_bytes"]
|
||||
|
||||
async def test_get_aggregated_storage_stats(self):
|
||||
with patch(
|
||||
"EvoScientist.sessions.get_db_path",
|
||||
return_value=_mock_path(self.db_path),
|
||||
):
|
||||
stats = await get_aggregated_storage_stats()
|
||||
assert stats["thread_count"] == 2
|
||||
assert stats["checkpoint_count"] == 8
|
||||
assert stats["thread_depth"]["max"] == 5
|
||||
assert stats["thread_depth"]["min"] == 3
|
||||
|
||||
async def test_list_all_session_db_paths(self):
|
||||
with patch(
|
||||
"EvoScientist.sessions.get_db_path",
|
||||
return_value=_mock_path(self.db_path),
|
||||
):
|
||||
paths = list_all_session_db_paths()
|
||||
assert len(paths) == 1
|
||||
assert str(paths[0]) == self.db_path
|
||||
|
||||
missing = os.path.join(self._tmpdir, "nope.db")
|
||||
with patch(
|
||||
"EvoScientist.sessions.get_db_path",
|
||||
return_value=_mock_path(missing),
|
||||
):
|
||||
assert list_all_session_db_paths() == []
|
||||
|
||||
|
||||
if __name__ == "__main__":
|
||||
unittest.main()
|
||||
|
||||
Reference in New Issue
Block a user