diff --git a/api/config.py b/api/config.py index 99c1c8f..7bcb11e 100644 --- a/api/config.py +++ b/api/config.py @@ -17,9 +17,12 @@ class Settings(BaseSettings): # that need the authoritative hub must be able to tell the difference. # Answering on a port is not evidence of authority — that assumption cost # seven weeks of onboarding (CUST-WP-0067-T03, ADR-010). - instance_role: Literal["primary", "cache", "unknown"] = "unknown" + state_hub_instance_role: Literal["primary", "cache", "unknown"] = "unknown" + # Env vars are STATE_HUB_INSTANCE_ROLE / STATE_HUB_INSTANCE_LABEL — the + # field name *is* the binding, so renaming either breaks the deployment + # silently. Covered by tests/test_instance_identity.py. # Free-form label to name *which* instance answered, e.g. "railiance01". - instance_label: str | None = None + state_hub_instance_label: str | None = None debug: bool = False state_hub_report_dir: str = "reports/recently-on-scope" state_hub_markitect_cli_path: str | None = None diff --git a/api/routers/state.py b/api/routers/state.py index 319c849..bbc231e 100644 --- a/api/routers/state.py +++ b/api/routers/state.py @@ -1113,8 +1113,8 @@ async def health_check(session: AsyncSession = Depends(get_session)) -> dict: "status": "ok", "db": "connected", # Identity, so a caller can verify it reached the hub it meant to. - "instance_role": settings.instance_role, - "instance_label": settings.instance_label, + "instance_role": settings.state_hub_instance_role, + "instance_label": settings.state_hub_instance_label, } except Exception as exc: return JSONResponse( diff --git a/tests/test_instance_identity.py b/tests/test_instance_identity.py new file mode 100644 index 0000000..6ed5b4e --- /dev/null +++ b/tests/test_instance_identity.py @@ -0,0 +1,38 @@ +"""The instance-identity env vars must actually bind (CUST-WP-0067-T03). + +The chart sets STATE_HUB_INSTANCE_ROLE. pydantic-settings derives the env var +from the *field name*, so a field called `instance_role` silently ignores it and +the hub reports "unknown" while the ConfigMap says "primary" — config that is +declared but never reaches what it configures, which is the entire failure this +workplan exists to close. +""" + +from __future__ import annotations + +import pytest + +from api.config import Settings + + +@pytest.mark.parametrize( + "env_name,attr,value", + [ + ("STATE_HUB_INSTANCE_ROLE", "state_hub_instance_role", "primary"), + ("STATE_HUB_INSTANCE_LABEL", "state_hub_instance_label", "railiance01"), + ], +) +def test_env_var_binds_to_setting(monkeypatch, env_name, attr, value): + monkeypatch.setenv(env_name, value) + assert getattr(Settings(), attr) == value + + +def test_role_defaults_to_unknown(monkeypatch): + """An instance that has not declared itself is not the primary.""" + monkeypatch.delenv("STATE_HUB_INSTANCE_ROLE", raising=False) + assert Settings().state_hub_instance_role == "unknown" + + +def test_role_rejects_unknown_values(monkeypatch): + monkeypatch.setenv("STATE_HUB_INSTANCE_ROLE", "authoritative") + with pytest.raises(Exception): + Settings()