"""Tests for the required-mode migration gate.""" from __future__ import annotations from types import SimpleNamespace import pytest from EvoScientist.scope_registry import deployment_id_for_workspace, get_scope_registry from EvoScientist.workspace_cutover import ( run_workspace_cutover, verify_required_cutover, ) class _Threads: def __init__(self, threads: list[dict] | None = None) -> None: self.updated: list[tuple[str, dict]] = [] self._threads = threads or [ { "thread_id": "primary-thread", "metadata": {"graph_id": "EvoScientist"}, } ] def search(self, *, limit: int, offset: int = 0): assert limit == 100 assert offset == 0 return self._threads def update(self, thread_id: str, *, metadata: dict): self.updated.append((thread_id, metadata)) class _Crons: def __init__(self, crons: list[dict] | None = None) -> None: self._crons = crons or [] self.updated: list[tuple[str, bool]] = [] def search(self, *, limit: int): assert limit == 1000 return self._crons def update(self, cron_id: str, *, enabled: bool): self.updated.append((cron_id, enabled)) class _Runs: def __init__(self, runs: dict[str, list[dict]] | None = None) -> None: self.runs = runs or {} self.cancelled: list[tuple[str, str]] = [] def list(self, *, thread_id: str, limit: int): assert limit == 1000 return self.runs.get(thread_id, []) def cancel(self, thread_id: str, run_id: str, *, wait: bool, action: str): assert wait is True assert action == "interrupt" self.cancelled.append((thread_id, run_id)) for run in self.runs.get(thread_id, []): if run["run_id"] == run_id: run["status"] = "interrupted" def test_cutover_writes_a_verified_passing_report(tmp_path, monkeypatch): import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() monkeypatch.delenv("EVOSCIENTIST_DEPLOYMENT_ID", raising=False) client = SimpleNamespace(threads=_Threads(), crons=_Crons(), runs=_Runs()) report = run_workspace_cutover( workspace_root=tmp_path, client=client, browser_sdk_gate=True ) assert report["status"] == "passed" assert client.threads.updated[0][1]["workspace_scope_id"] verify_required_cutover(tmp_path) def test_required_cutover_rejects_missing_report(tmp_path): with pytest.raises(RuntimeError, match="workspace-cutover"): verify_required_cutover(tmp_path) def test_cutover_quarantines_unscoped_derived_threads(tmp_path, monkeypatch): import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() monkeypatch.delenv("EVOSCIENTIST_DEPLOYMENT_ID", raising=False) threads = _Threads( [ {"thread_id": "primary-thread", "metadata": {"graph_id": "EvoScientist"}}, {"thread_id": "legacy-child", "metadata": {"agent_name": "worker"}}, ] ) runs = _Runs({"legacy-child": [{"run_id": "run-child", "status": "running"}]}) report = run_workspace_cutover( workspace_root=tmp_path, client=SimpleNamespace(threads=threads, crons=_Crons(), runs=runs), browser_sdk_gate=True, ) assert report["status"] == "passed" assert runs.cancelled == [("legacy-child", "run-child")] assert report["quarantined_derived_threads"] == [ { "thread_id": "legacy-child", "cancelled_run_ids": ["run-child"], "reason": "unscoped-derived-thread", } ] derived_update = next( update for update in threads.updated if update[0] == "legacy-child" ) assert ( derived_update[1]["workspace_quarantine"]["reason"] == "unscoped-derived-thread" ) def test_cutover_accepts_registered_scoped_derived_thread(tmp_path, monkeypatch): import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() monkeypatch.delenv("EVOSCIENTIST_DEPLOYMENT_ID", raising=False) deployment_id = deployment_id_for_workspace(tmp_path) registry = get_scope_registry(tmp_path) scope = registry.provision(deployment_id, "primary-thread") owner = registry.register_owner( deployment_id, scope.scope_id, owner_type="async_thread", resource_id="registered-child", parent_owner_id=scope.primary_owner_id, state="active", ) threads = _Threads( [ {"thread_id": "primary-thread", "metadata": {"graph_id": "EvoScientist"}}, { "thread_id": "registered-child", "metadata": { "workspace_scope_id": scope.scope_id, "workspace_scope_owner_id": owner.owner_id, "workspace_deployment_id": deployment_id, }, }, ] ) report = run_workspace_cutover( workspace_root=tmp_path, client=SimpleNamespace(threads=threads, crons=_Crons(), runs=_Runs()), browser_sdk_gate=True, ) assert report["status"] == "passed" assert report["validated_derived_threads"] == ["registered-child"] assert report["quarantined_derived_threads"] == [] def test_cutover_quarantines_scoped_thread_without_registry_owner( tmp_path, monkeypatch ): import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() monkeypatch.delenv("EVOSCIENTIST_DEPLOYMENT_ID", raising=False) threads = _Threads( [ {"thread_id": "primary-thread", "metadata": {"graph_id": "EvoScientist"}}, { "thread_id": "forged-child", "metadata": { "workspace_scope_id": "00000000-0000-4000-8000-000000000001", "workspace_scope_owner_id": "00000000-0000-4000-8000-000000000002", }, }, ] ) report = run_workspace_cutover( workspace_root=tmp_path, client=SimpleNamespace(threads=threads, crons=_Crons(), runs=_Runs()), browser_sdk_gate=True, ) assert report["status"] == "passed" assert report["invalid_scoped_derived_threads"][0]["thread_id"] == "forged-child" update = next(item for item in threads.updated if item[0] == "forged-child") assert "workspace_scope_id" not in update[1] assert ( update[1]["workspace_quarantine"]["reason"] == "invalid-scoped-derived-thread" ) def test_cutover_disables_scoped_cron_without_registry_owner(tmp_path, monkeypatch): import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() monkeypatch.delenv("EVOSCIENTIST_DEPLOYMENT_ID", raising=False) crons = _Crons( [ { "cron_id": "forged-cron", "metadata": { "run_kind": "scheduled_task", "workspace_scope_id": "00000000-0000-4000-8000-000000000001", }, } ] ) report = run_workspace_cutover( workspace_root=tmp_path, client=SimpleNamespace(threads=_Threads(), crons=crons, runs=_Runs()), browser_sdk_gate=True, ) assert report["status"] == "passed" assert crons.updated == [("forged-cron", False)] assert report["invalid_scoped_crons"][0]["cron_id"] == "forged-cron"