262 lines
9.9 KiB
Python
262 lines
9.9 KiB
Python
"""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
|