"""What a worker needs to do its job. One frozen bag of collaborators, constructed once in main() and passed down. Handlers take `deps` rather than reaching for globals, which is the entire reason the worker tests run in milliseconds against a FakeProvisioner instead of needing a cluster. """ from __future__ import annotations from dataclasses import dataclass from typing import Protocol from svcforge_core.adapters.clock import Clock from svcforge_core.adapters.helm import Provisioner from svcforge_core.adapters.notify import Notifier from svcforge_core.domain.models import CatalogEntry from svcforge_core.repo.db import DictPool from svcforge_core.repo.instances import InstanceRepo from svcforge_core.repo.tasks import TaskRepo from svcforge_core.settings import Settings class NamespaceEnsurer(Protocol): """The one thing the worker needs from kubectl. Narrower than `KubectlClient` on purpose: the handler creates namespaces and does nothing else with the cluster, so that is the whole interface. Satisfied by `KubectlClient` in production and by a fake in tests, which is what keeps the worker suite running in milliseconds with no cluster. """ async def ensure_namespace(self, ns: str, labels: dict[str, str] | None = None) -> None: """Create the namespace if absent. Idempotent — safe on every retry.""" ... @dataclass(frozen=True) class WorkerDeps: """Everything a handler is allowed to touch.""" pool: DictPool instances: InstanceRepo tasks: TaskRepo provisioner: Provisioner # Creates the tenant namespace before helm is pointed at it. `helm --namespace X` # does NOT create X, so without this the very first provision for a new team fails # with "namespace not found" — the one path that is guaranteed untested by a fake. namespaces: NamespaceEnsurer notifier: Notifier clock: Clock catalog: dict[str, CatalogEntry] settings: Settings