Files
next-tjwater-server/tests/unit/test_worker_project_scopes.py
jiang 1d061f6099
Generic Container CI/CD / test-build-publish (push) Failing after 6s
Server CI/CD v2 / build-test-publish-and-deploy (push) Failing after 6s
feat: add project-scoped operations
2026-10-22 18:40:20 +08:00

102 lines
4.2 KiB
Python

from dataclasses import replace
from uuid import uuid4
import httpx
import pytest
from scripts import run_operational_worker as worker
from scripts import sync_peripheral_work_orders as sync
@pytest.fixture
def anyio_backend():
return "asyncio"
@pytest.fixture
def settings():
projects = (str(uuid4()), str(uuid4()))
return worker.WorkerSettings("http://server.test", "http://agent.test", projects[0],
bearer="TEST_ONLY", project_ids=projects)
def test_explicit_project_list_overrides_single_project_and_deduplicates(monkeypatch, settings):
monkeypatch.setattr(worker, "load_dotenv", lambda *args, **kwargs: None)
monkeypatch.setenv("OPERATIONAL_WORKER_PROJECT_IDS", ", ".join(settings.project_ids + settings.project_ids[:1]))
monkeypatch.setenv("OPERATIONAL_WORKER_PROJECT_ID", "ignored-legacy-value")
monkeypatch.setenv("OPERATIONAL_WORKER_BEARER", "TEST_ONLY")
parsed = worker.WorkerSettings.from_environment()
assert parsed.project_ids == settings.project_ids
assert tuple(scope.project_id for scope in parsed.project_scopes()) == settings.project_ids
assert all(not scope.project_ids for scope in parsed.project_scopes())
monkeypatch.setenv("OPERATIONAL_WORKER_PROJECT_IDS", settings.project_ids[0] + ",invalid")
with pytest.raises(ValueError):
worker.WorkerSettings.from_environment()
def test_existing_single_project_configuration_remains_supported(monkeypatch, settings):
monkeypatch.setattr(worker, "load_dotenv", lambda *args, **kwargs: None)
monkeypatch.delenv("OPERATIONAL_WORKER_PROJECT_IDS", raising=False)
monkeypatch.setenv("OPERATIONAL_WORKER_PROJECT_ID", settings.project_id)
monkeypatch.setenv("OPERATIONAL_WORKER_BEARER", "TEST_ONLY")
assert worker.WorkerSettings.from_environment().project_scopes()[0].project_id == settings.project_id
assert replace(settings, project_ids=()).project_scopes() == (replace(settings, project_ids=()),)
@pytest.mark.anyio
async def test_multi_project_worker_reconciles_other_projects_when_one_fails(monkeypatch, settings):
seen = []
class FakeWorker:
def __init__(self, scope, client):
self.scope = scope
async def tick(self):
seen.append(self.scope.project_id)
if self.scope.project_id == settings.project_ids[0]:
raise httpx.ConnectError("test failure")
return 0
monkeypatch.setattr(worker.WorkerSettings, "from_environment", lambda: settings)
monkeypatch.setattr(worker, "OperationalWorker", FakeWorker)
with pytest.raises(SystemExit) as result:
await worker.run(once=True)
assert result.value.code == 1
assert tuple(seen) == settings.project_ids
@pytest.mark.anyio
@pytest.mark.parametrize("once", [True, False])
async def test_sync_entrypoint_keeps_project_headers_and_cursors_separate(monkeypatch, settings, once):
seen = []
async def check_scope(client, scope, url, credentials):
seen.append((scope.project_id, (await credentials.headers(client))["X-Project-Id"]))
return 0
class FakeSynchronizer:
def __init__(self, client, scope, url, credentials):
self.args = client, scope, url, credentials
async def serve(self, interval):
return await check_scope(*self.args)
monkeypatch.setattr(worker.WorkerSettings, "from_environment", lambda: settings)
monkeypatch.setenv("OPERATIONAL_ACTION_PERIPHERAL_URL", "http://peripheral.test")
monkeypatch.setattr(sync, "sync_once", check_scope)
monkeypatch.setattr(sync, "PeripheralWorkOrderSynchronizer", FakeSynchronizer)
await sync.run(once=once)
assert seen == [(project, project) for project in settings.project_ids]
@pytest.mark.anyio
async def test_project_workers_have_independent_tokens_sessions_and_message_cursors(settings):
async with httpx.AsyncClient() as client:
first, second = [worker.OperationalWorker(scope, client) for scope in settings.project_scopes()]
first.credentials._token = "FIRST_TEST_TOKEN"
first._agent_sessions["run-1"] = "session-1"
first._message_cursor = "cursor-1"
assert second.credentials._token == ""
assert second._agent_sessions == {}
assert second._message_cursor is None