An "unknown"/empty node_name silently produced evt-unknown-* event files. One landed in events/piha/ and, being lexically greater than every evt-piha-* name, pinned the observer's (then lexical) checkpoint and blocked PIHA for ~34 days. Defense in depth, two layers, both refuse rather than emit poison: - config.py: node_name field_validator rejects ""/"unknown" (case-insensitive, trimmed); validate_default=True so the "unknown" default itself is rejected when NODE_NAME never reaches the process. main() catches ValidationError and exits 1 with a clear FATAL message instead of a raw traceback. - event_emitter.py: EventEmitter.__init__ guards node_name at the exact poison site (node_name is embedded in the filename), so no future call path can reintroduce evt-unknown-*. Precedence unchanged and correct: Settings.load() uses os.environ.setdefault, so env NODE_NAME wins over YAML; YAML only supplies node_name when env is absent. api.py never writes event files (only /health, /trigger), so its _node_name global cannot produce poison — verified, left as-is. Tests (services/ha-diag-agent/tests/): new test_config.py (env load, env>YAML precedence, fail-fast on unknown/empty/whitespace/default/YAML-unknown) and EventEmitter guard tests. Full unit suite: 139 passed, 0 regressions. Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
142 lines
4.7 KiB
Python
142 lines
4.7 KiB
Python
"""Tests for Settings / node_name fail-fast.
|
|
|
|
Regression coverage for the evt-unknown-* poison bug: a node_name of "unknown"
|
|
(or empty) produced event files that pinned the observer checkpoint and blocked
|
|
PIHA for ~34 days (see fix d5139c9). The agent must now refuse to start rather
|
|
than ever emit such a file.
|
|
"""
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
from pydantic import ValidationError
|
|
|
|
from ha_diag import config
|
|
from ha_diag.config import Settings
|
|
|
|
|
|
@pytest.fixture(autouse=True)
|
|
def _isolate_environ():
|
|
"""Snapshot os.environ — Settings.load() mutates it via setdefault()."""
|
|
saved = dict(os.environ)
|
|
yield
|
|
os.environ.clear()
|
|
os.environ.update(saved)
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# node_name loaded from env NODE_NAME
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_node_name_from_env(monkeypatch):
|
|
monkeypatch.setenv("NODE_NAME", "piha")
|
|
settings = Settings()
|
|
assert settings.node_name == "piha"
|
|
|
|
|
|
def test_node_name_from_env_case_insensitive_var(monkeypatch):
|
|
# BaseSettings is case-insensitive: lowercase env var maps to the field too.
|
|
monkeypatch.delenv("NODE_NAME", raising=False)
|
|
monkeypatch.setenv("node_name", "chelsty-infra")
|
|
settings = Settings()
|
|
assert settings.node_name == "chelsty-infra"
|
|
|
|
|
|
def test_node_name_whitespace_is_stripped(monkeypatch):
|
|
monkeypatch.setenv("NODE_NAME", " piha ")
|
|
settings = Settings()
|
|
assert settings.node_name == "piha"
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# fail-fast: unknown / empty node_name refuses to start
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_explicit_unknown_node_name_refuses(monkeypatch):
|
|
monkeypatch.delenv("NODE_NAME", raising=False)
|
|
with pytest.raises(ValidationError) as exc:
|
|
Settings(node_name="unknown")
|
|
assert "refusing to start" in str(exc.value)
|
|
|
|
|
|
def test_unknown_case_insensitive_refuses(monkeypatch):
|
|
monkeypatch.delenv("NODE_NAME", raising=False)
|
|
with pytest.raises(ValidationError):
|
|
Settings(node_name="UNKNOWN")
|
|
|
|
|
|
def test_empty_node_name_refuses(monkeypatch):
|
|
monkeypatch.delenv("NODE_NAME", raising=False)
|
|
with pytest.raises(ValidationError):
|
|
Settings(node_name="")
|
|
|
|
|
|
def test_whitespace_only_node_name_refuses(monkeypatch):
|
|
monkeypatch.delenv("NODE_NAME", raising=False)
|
|
with pytest.raises(ValidationError):
|
|
Settings(node_name=" ")
|
|
|
|
|
|
def test_env_unknown_refuses(monkeypatch):
|
|
monkeypatch.setenv("NODE_NAME", "unknown")
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
|
|
def test_default_without_env_refuses(monkeypatch):
|
|
# No NODE_NAME anywhere → the "unknown" default must be rejected, not used.
|
|
monkeypatch.delenv("NODE_NAME", raising=False)
|
|
monkeypatch.delenv("node_name", raising=False)
|
|
with pytest.raises(ValidationError):
|
|
Settings()
|
|
|
|
|
|
# ---------------------------------------------------------------------------
|
|
# precedence: env wins over YAML for node_name
|
|
# ---------------------------------------------------------------------------
|
|
|
|
|
|
def test_env_wins_over_yaml(monkeypatch, tmp_path: Path):
|
|
yaml_file = tmp_path / "ha-diag-agent.yaml"
|
|
yaml_file.write_text("node_name: solaria\n")
|
|
monkeypatch.setattr(config, "_CONFIG_YAML", yaml_file)
|
|
monkeypatch.setenv("NODE_NAME", "piha")
|
|
|
|
settings = Settings.load()
|
|
# env value must win; YAML only supplies a default when env is absent.
|
|
assert settings.node_name == "piha"
|
|
|
|
|
|
def test_yaml_supplies_node_name_when_env_absent(monkeypatch, tmp_path: Path):
|
|
yaml_file = tmp_path / "ha-diag-agent.yaml"
|
|
yaml_file.write_text("node_name: chelsty-infra\n")
|
|
monkeypatch.setattr(config, "_CONFIG_YAML", yaml_file)
|
|
monkeypatch.delenv("NODE_NAME", raising=False)
|
|
monkeypatch.delenv("node_name", raising=False)
|
|
|
|
settings = Settings.load()
|
|
assert settings.node_name == "chelsty-infra"
|
|
|
|
|
|
def test_yaml_unknown_still_refuses(monkeypatch, tmp_path: Path):
|
|
# Even if YAML carries the poison value, fail-fast must still fire.
|
|
yaml_file = tmp_path / "ha-diag-agent.yaml"
|
|
yaml_file.write_text("node_name: unknown\n")
|
|
monkeypatch.setattr(config, "_CONFIG_YAML", yaml_file)
|
|
monkeypatch.delenv("NODE_NAME", raising=False)
|
|
monkeypatch.delenv("node_name", raising=False)
|
|
|
|
with pytest.raises(ValidationError):
|
|
Settings.load()
|
|
|
|
|
|
def test_load_valid_node_name(monkeypatch, tmp_path: Path):
|
|
monkeypatch.setattr(config, "_CONFIG_YAML", tmp_path / "does-not-exist.yaml")
|
|
monkeypatch.setenv("NODE_NAME", "piha")
|
|
settings = Settings.load()
|
|
assert settings.node_name == "piha"
|