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