117 lines
4.3 KiB
Python
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"]
|