Files
EvoScientist/tests/test_workspace_scope_http.py
T
m4 421a664336 feat(runtime)!: complete legacy removal, local snapshot entries, and TTL cleanup
- Remove legacy provider profiles, admin-token auth, /model command,
  model picker widget, and config.yaml LLM fields (design doc section 10)
- Wire CLI/channels/cron and async sub-agents through the local snapshot
  entry; run creation rejects model config outside runtime_snapshot_id
- Add periodic run-snapshot TTL cleanup to the config service lifespan
- Isolate tests from the real config dir and activate the registry where
  run/model paths fail closed in bootstrap

Co-Authored-By: Claude Opus 4.7 <noreply@anthropic.com>
2026-07-21 18:10:23 +08:00

269 lines
10 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
@pytest.fixture(autouse=True)
def _active_registry(active_snapshot_runtime):
"""Run entry points freeze a local snapshot per run; that fails closed
against the default bootstrap registry, so activate it for these tests."""
return active_snapshot_runtime
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