Files
EvoScientist/tests/test_workspace_maintenance.py
T

117 lines
4.3 KiB
Python

"""Lifecycle maintenance tests for scoped conversation workspaces."""
from __future__ import annotations
import os
import sqlite3
from datetime import UTC, datetime, timedelta
from pathlib import Path
from types import SimpleNamespace
from EvoScientist.scope_registry import deployment_id_for_workspace, get_scope_registry
from EvoScientist.workspace_maintenance import run_workspace_maintenance
from EvoScientist.workspace_scope import provision_conversation_scope
class _Threads:
def __init__(self) -> None:
self.deleted: list[str] = []
def delete(self, thread_id: str) -> None:
self.deleted.append(thread_id)
def get_state(self, thread_id: str) -> dict:
return {"values": {"messages": []}}
class _Runs:
def list(self, *, thread_id: str, limit: int):
assert limit == 1000
return []
def _age_scope(workspace_root: Path, scope_id: str, when: datetime) -> None:
registry = get_scope_registry(workspace_root)
with sqlite3.connect(registry.path) as database:
database.execute(
"UPDATE scopes SET created_at = ?, updated_at = ? WHERE scope_id = ?",
(when.isoformat(), when.isoformat(), scope_id),
)
def test_maintenance_deletes_stale_draft_and_purges_old_trash(tmp_path, monkeypatch):
import EvoScientist.scope_registry as registry_module
registry_module._registry_cache.clear()
monkeypatch.delenv("EVOSCIENTIST_DEPLOYMENT_ID", raising=False)
scope = provision_conversation_scope("draft-thread", workspace_root=tmp_path)
_age_scope(tmp_path, scope.scope_id, datetime(2026, 7, 1, tzinfo=UTC))
files = tmp_path / ".evoscientist" / "conversations" / scope.scope_id / "files"
files.mkdir(parents=True, exist_ok=True)
old_trash = tmp_path / ".evoscientist" / "trash" / "old-scope"
old_trash.mkdir(parents=True)
old_timestamp = datetime(2026, 7, 1, tzinfo=UTC).timestamp()
os.utime(old_trash, (old_timestamp, old_timestamp))
threads = _Threads()
report = run_workspace_maintenance(
workspace_root=tmp_path,
client=SimpleNamespace(threads=threads, runs=_Runs()),
now=datetime(2026, 7, 10, tzinfo=UTC),
)
assert report["errors"] == []
assert report["deleted_drafts"] == ["draft-thread"]
assert report["purged_trash"] == 1
assert threads.deleted == ["draft-thread"]
registry = get_scope_registry(tmp_path)
deployment_id = deployment_id_for_workspace(tmp_path)
assert registry.get(deployment_id, scope.scope_id).state == "deleted"
assert not old_trash.exists()
assert any((tmp_path / ".evoscientist" / "trash").iterdir())
def test_maintenance_leaves_draft_with_non_primary_owner(tmp_path, monkeypatch):
import EvoScientist.scope_registry as registry_module
registry_module._registry_cache.clear()
monkeypatch.delenv("EVOSCIENTIST_DEPLOYMENT_ID", raising=False)
scope = provision_conversation_scope("draft-thread", workspace_root=tmp_path)
_age_scope(tmp_path, scope.scope_id, datetime.now(UTC) - timedelta(days=2))
registry = get_scope_registry(tmp_path)
registry.register_owner(
deployment_id_for_workspace(tmp_path),
scope.scope_id,
owner_type="unknown-legacy-owner",
state="active",
)
report = run_workspace_maintenance(
workspace_root=tmp_path,
client=SimpleNamespace(threads=_Threads(), runs=_Runs()),
)
assert report["deleted_drafts"] == []
assert report["skipped_drafts"] == ["draft-thread"]
def test_maintenance_leaves_draft_that_has_messages(tmp_path, monkeypatch):
import EvoScientist.scope_registry as registry_module
class _ThreadWithMessages(_Threads):
def get_state(self, thread_id: str) -> dict:
return {"values": {"messages": [{"content": "keep this draft"}]}}
registry_module._registry_cache.clear()
monkeypatch.delenv("EVOSCIENTIST_DEPLOYMENT_ID", raising=False)
scope = provision_conversation_scope("draft-thread", workspace_root=tmp_path)
_age_scope(tmp_path, scope.scope_id, datetime.now(UTC) - timedelta(days=2))
report = run_workspace_maintenance(
workspace_root=tmp_path,
client=SimpleNamespace(threads=_ThreadWithMessages(), runs=_Runs()),
)
assert report["deleted_drafts"] == []
assert report["skipped_drafts"] == ["draft-thread"]