"""Tests for the server-only workspace scope registry routes.""" from __future__ import annotations import asyncio from types import SimpleNamespace import pytest from blockbuster import BlockBuster from starlette.testclient import TestClient from EvoScientist import paths from EvoScientist.langgraph_dev.http import app from EvoScientist.workspace_scope import DeferredScopedBackend, create_workspace_backend def test_scope_routes_require_service_token(monkeypatch): monkeypatch.delenv("EVOSCIENTIST_BACKEND_SERVICE_TOKEN", raising=False) with TestClient(app) as client: response = client.post( "/internal/workspace-scopes/provision", json={"thread_id": "thread-a"} ) assert response.status_code == 503 def test_scope_provision_and_lookup(tmp_path, monkeypatch): monkeypatch.setattr(paths, "WORKSPACE_ROOT", tmp_path) monkeypatch.setenv("EVOSCIENTIST_BACKEND_SERVICE_TOKEN", "scope-test-token") monkeypatch.setenv("EVOSCIENTIST_DEPLOYMENT_ID", "deployment-test") import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() headers = {"Authorization": "Bearer scope-test-token"} with TestClient(app) as client: created = client.post( "/internal/workspace-scopes/provision", json={"thread_id": "thread-a"}, headers=headers, ) assert created.status_code == 201 payload = created.json() looked_up = client.get( "/internal/workspace-scopes/by-thread/thread-a", headers=headers ) assert looked_up.status_code == 200 assert looked_up.json()["scope_id"] == payload["scope_id"] assert ( tmp_path / ".evoscientist" / "conversations" / payload["scope_id"] / "files" ).is_dir() def test_scope_registry_is_constructed_off_the_asgi_event_loop(tmp_path, monkeypatch): monkeypatch.setattr(paths, "WORKSPACE_ROOT", tmp_path) monkeypatch.setenv("EVOSCIENTIST_BACKEND_SERVICE_TOKEN", "scope-test-token") monkeypatch.setenv("EVOSCIENTIST_DEPLOYMENT_ID", "deployment-test") import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() original = registry_module.get_scope_registry def off_event_loop_registry(*args, **kwargs): with pytest.raises(RuntimeError): asyncio.get_running_loop() return original(*args, **kwargs) monkeypatch.setattr(registry_module, "get_scope_registry", off_event_loop_registry) headers = {"Authorization": "Bearer scope-test-token"} with TestClient(app) as client: created = client.post( "/internal/workspace-scopes/provision", json={"thread_id": "thread-a"}, headers=headers, ) looked_up = client.get( "/internal/workspace-scopes/by-thread/thread-a", headers=headers ) assert created.status_code == 201 assert looked_up.status_code == 200 def test_scope_turn_routes_are_idempotent(tmp_path, monkeypatch): monkeypatch.setattr(paths, "WORKSPACE_ROOT", tmp_path) monkeypatch.setenv("EVOSCIENTIST_BACKEND_SERVICE_TOKEN", "scope-test-token") monkeypatch.setenv("EVOSCIENTIST_DEPLOYMENT_ID", "deployment-test") import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() headers = {"Authorization": "Bearer scope-test-token"} turn_id = "00000000-0000-4000-8000-000000000001" with TestClient(app) as client: scope = client.post( "/internal/workspace-scopes/provision", json={"thread_id": "thread-a"}, headers=headers, ).json() reserved = client.post( f"/internal/workspace-scopes/{scope['scope_id']}/turns/reserve", json={"turn_id": turn_id, "request_hash": "hash-a"}, headers=headers, ) assert reserved.status_code == 201 bound = client.patch( f"/internal/workspace-scopes/{scope['scope_id']}/turns/{turn_id}", json={"run_id": "run-a"}, headers=headers, ) assert bound.status_code == 200 retried = client.post( f"/internal/workspace-scopes/{scope['scope_id']}/turns/reserve", json={"turn_id": turn_id, "request_hash": "hash-a"}, headers=headers, ) assert retried.status_code == 201 assert retried.json()["run_id"] == "run-a" def test_scope_run_routes_support_approval_resume_and_dedupe_interrupts( tmp_path, monkeypatch ): monkeypatch.setattr(paths, "WORKSPACE_ROOT", tmp_path) monkeypatch.setenv("EVOSCIENTIST_BACKEND_SERVICE_TOKEN", "scope-test-token") monkeypatch.setenv("EVOSCIENTIST_DEPLOYMENT_ID", "deployment-test") import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() headers = {"Authorization": "Bearer scope-test-token"} turn_id = "00000000-0000-4000-8000-000000000001" initial_request_id = "00000000-0000-4000-8000-000000000002" resume_request_id = "00000000-0000-4000-8000-000000000003" duplicate_interrupt_request_id = "00000000-0000-4000-8000-000000000004" with TestClient(app) as client: scope = client.post( "/internal/workspace-scopes/provision", json={"thread_id": "thread-a"}, headers=headers, ).json() initial = client.post( f"/internal/workspace-scopes/{scope['scope_id']}/runs/reserve", json={ "run_request_id": initial_request_id, "turn_id": turn_id, "request_hash": "initial", }, headers=headers, ) resume = client.post( f"/internal/workspace-scopes/{scope['scope_id']}/runs/reserve", json={ "run_request_id": resume_request_id, "turn_id": turn_id, "request_hash": "approve", "interrupt_key": "interrupt-a", }, headers=headers, ) retried_resume = client.post( f"/internal/workspace-scopes/{scope['scope_id']}/runs/reserve", json={ "run_request_id": resume_request_id, "turn_id": turn_id, "request_hash": "approve", "interrupt_key": "interrupt-a", }, headers=headers, ) duplicate_interrupt = client.post( f"/internal/workspace-scopes/{scope['scope_id']}/runs/reserve", json={ "run_request_id": duplicate_interrupt_request_id, "turn_id": turn_id, "request_hash": "another-decision", "interrupt_key": "interrupt-a", }, headers=headers, ) assert initial.status_code == 201 assert resume.status_code == 201 assert initial.json()["run_owner_id"] != resume.json()["run_owner_id"] assert retried_resume.status_code == 201 assert retried_resume.json()["run_owner_id"] == resume.json()["run_owner_id"] assert duplicate_interrupt.status_code == 409 assert duplicate_interrupt.json()["code"] == "interrupt_already_resolved" async def test_reserved_turn_creates_an_async_safe_scoped_backend( tmp_path, monkeypatch ): monkeypatch.setattr(paths, "WORKSPACE_ROOT", tmp_path) monkeypatch.setenv("EVOSCIENTIST_BACKEND_SERVICE_TOKEN", "scope-test-token") monkeypatch.setenv("EVOSCIENTIST_DEPLOYMENT_ID", "deployment-test") import EvoScientist.scope_registry as registry_module import EvoScientist.workspace_scope as workspace_scope_module registry_module._registry_cache.clear() headers = {"Authorization": "Bearer scope-test-token"} turn_id = "00000000-0000-4000-8000-000000000001" with TestClient(app) as client: scope = client.post( "/internal/workspace-scopes/provision", json={"thread_id": "thread-a"}, headers=headers, ).json() reservation = client.post( f"/internal/workspace-scopes/{scope['scope_id']}/turns/reserve", json={"turn_id": turn_id, "request_hash": "hash-a"}, headers=headers, ).json() runtime = SimpleNamespace( config={ "configurable": { "thread_id": "thread-a", "workspace_scope_id": scope["scope_id"], "workspace_scope_owner_id": reservation["run_owner_id"], "workspace_scope_revision": scope["revision"], "workspace_deployment_id": scope["deployment_id"], } } ) blocker = BlockBuster(scanned_modules=[workspace_scope_module, registry_module]) blocker.activate() try: backend = create_workspace_backend( runtime, legacy_backend=lambda: pytest.fail("scoped runtime used legacy backend"), ) result = await backend.als("/") finally: blocker.deactivate() assert isinstance(backend, DeferredScopedBackend) assert result.error is None def test_scope_owner_list_and_cutover_lock_routes(tmp_path, monkeypatch): monkeypatch.setattr(paths, "WORKSPACE_ROOT", tmp_path) monkeypatch.setenv("EVOSCIENTIST_BACKEND_SERVICE_TOKEN", "scope-test-token") monkeypatch.setenv("EVOSCIENTIST_DEPLOYMENT_ID", "deployment-test") import EvoScientist.scope_registry as registry_module registry_module._registry_cache.clear() headers = {"Authorization": "Bearer scope-test-token"} with TestClient(app) as client: created = client.post( "/internal/workspace-scopes/provision", json={"thread_id": "thread-a"}, headers=headers, ).json() owners = client.get( f"/internal/workspace-scopes/{created['scope_id']}/owners", headers=headers ) lock = client.get("/internal/workspace-scopes/cutover-lock", headers=headers) assert owners.status_code == 200 assert owners.json()["owners"][0]["owner_type"] == "primary_thread" assert lock.status_code == 200 assert lock.json()["active"] is False