Files
dashboards/tests/test_config.py
T
ducoterra 5f3ed9bcd4 phase: 23_llm_rate_responsiveness
Phase 23 final verification complete — all green.

```
VERIFIED (criteria)
1  /api/llm/snapshot windows ≈2 s live (1.98–2.00), /api/system window_seconds ≈10 s (9.996–10.005);
   /api/config llm.rate_window_seconds=2=LLM_INTERVAL_SECONDS; browser re-check ALL CHECKS PASSED
   (data/logs/23-window-{llm,system}.png — /llm prints Δ2.0s, /system footnote Δ10.0s)
2  Phase-13 pins unchanged: floor, 2×span ceiling, 8× jump gate + independent anchor — all pass
   (only import/span-line diffs in tests/; git diff confirms no assertion rewritten)
3  Live proof re-derived from artifacts: 780 rows, 0 unlabelled, windows 0.61–3.38 s, 5 starts +
   5 ends settled by poll 2 (0 unsettled), 1 m rollups max 42.3 vs raw 201.6, 0 over 300 tok/s;
   counterfactual 10 s window: median settle poll 3, |ln| 0.924 vs 0.037
4  uv run pytest: 1494 passed / 15 skipped · coverage 98.94% (floor 94) · ruff clean · pyright 0
   errors · node --check clean · bash .agents/validate.sh exit 0 · live 11 passed · store_perf 4 passed
   Dev server up, all nine sources ok. No git add/commit run; everything left in the working tree.

DOCS PASS — 4 inconsistencies fixed in AGENTS.md (last two runs above include them)
- UI conventions hero example still said the `.sub` footnote reads `Δ10.0s`; the only hero that
  prints a Δ is /llm's → now `Δ2.0s` on /llm, "at the current load" on /ups
- gap 9: "found 10 busy-edges" contradicted its own artifact (17 edges, 10 judged, 7 reported-not-
  judged) → 17; and the poll-2 ratios were a 3-of-5 cherry-pick → full five values both windows
- gap 12: the responsive-table fix is now scheduled — names 27_mobile_tables_and_audit
README, config table, .env.example, rates/prom docstrings, app/api bullets and the testing-policy
pins all re-checked against code and found consistent (store pragmas, budgets, throttle, fleet).

NOTES
- Advisory only, not phase 23: fastapi 0.153.0 → 0.156.8 available; dependencies untouched here,
  so rule 8's "update on purpose" is left to the operator.
- NOTES.md is empty in the working tree (its items became phases 24–27); left as-is, untracked-file
  rules forbid me removing it.
- Next phase: 24_dev_mtls_rollout (25–27 follow).
```
2026-10-21 08:04:21 -04:00

313 lines
14 KiB
Python

"""Configuration: env-only, no defaults that point at somebody else's machine."""
from __future__ import annotations
import importlib
import os
from dataclasses import FrozenInstanceError, fields
from typing import Any
import pytest
from dashboards import config
from dashboards.auth import PBKDF2_ITERATIONS
from dashboards.config import Settings, parse_targets
def test_parses_whitespace_and_comma_separated_targets() -> None:
targets, problems = parse_targets("a=http://h1:9120 b=http://h2:9120,c=http://h3:9120")
assert [t.name for t in targets] == ["a", "b", "c"]
assert [t.url for t in targets] == ["http://h1:9120", "http://h2:9120", "http://h3:9120"]
assert problems == []
def test_trailing_slash_is_stripped_so_urls_do_not_double_up() -> None:
targets, _ = parse_targets("a=http://h1:9120/")
assert targets[0].url == "http://h1:9120"
def test_one_bad_entry_does_not_sink_the_others() -> None:
targets, problems = parse_targets("good=http://h:1 broken-no-equals another=ftp://h:1")
assert [t.name for t in targets] == ["good"]
assert len(problems) == 2
assert any("not name=url" in p for p in problems)
assert any("http://" in p for p in problems)
def test_empty_input_yields_no_targets_and_no_noise() -> None:
assert parse_targets("") == ([], [])
assert parse_targets(" \n ") == ([], [])
def test_token_in_url_becomes_a_bearer_header_and_stays_out_of_the_display_url() -> None:
targets, problems = parse_targets("driveripper=http://SEKRIT@driveripper:9120")
assert problems == []
target = targets[0]
assert target.headers == {"Authorization": "Bearer SEKRIT"}
assert target.url == "http://driveripper:9120", "the token must not stay in the URL"
assert target.display_url == "http://driveripper:9120"
assert "SEKRIT" not in target.display_url
@pytest.mark.parametrize("raw,shown", [
("http://SEKRIT@host:9120", "http://host:9120"),
("https://user:pass@host:8000/metrics", "https://host:8000/metrics"),
("HTTP://SEKRIT@host:1", "HTTP://host:1"),
("http://host:9120", "http://host:9120"), # clean URLs pass through untouched
("", ""),
])
def test_strip_userinfo_is_the_one_redaction_every_url_leaves_by(raw: str, shown: str) -> None:
"""`strip_userinfo` is what `/api/config` and every `UpstreamError` message use, so it has
to survive the shapes a URL actually comes in: both schemes, `user:pass@`, a path, and the
common no-credentials case where it must change nothing (invariant 6).
"""
assert config.strip_userinfo(raw) == shown
def test_target_names_are_restricted_because_they_are_db_keys() -> None:
"""Names key the history DB and appear in URLs, so they are validated, not guessed.
Whitespace separates entries, which means "Bad Name=http://h" is two entries — both
reported, because the operator needs to see exactly what was rejected.
"""
targets, problems = parse_targets("Bad=http://h:1 /=http://h:1 ok-1_2=http://h:1")
assert [t.name for t in targets] == ["ok-1_2"]
assert len(problems) == 2
assert any("Bad" in p for p in problems)
_targets, problems = parse_targets("Bad Name=http://h:1")
assert len(problems) == 2, "a space splits the entry, so both halves are rejected"
def test_settings_default_to_no_sources_configured() -> None:
settings = Settings()
assert settings.host_targets == ()
assert settings.gufo_url == "" and settings.vllm_url == ""
assert settings.nut_host == ""
assert settings.nut_enabled is False
assert settings.any_source_enabled is False
def test_the_disk_source_defaults_to_a_five_minute_cadence() -> None:
"""SMART is not a per-second signal (phase 09), and the fetch waits for the agent's
own 20 s smartctl budget. Both are env-only, like every other interval."""
settings = Settings()
assert settings.disks_interval == 300.0
assert settings.disks_timeout == 30.0
def test_the_disk_cadence_is_an_env_var_like_every_other_one(monkeypatch: pytest.MonkeyPatch) -> None:
"""`DISKS_INTERVAL_SECONDS=60` is how an operator watches a failing drive closely."""
monkeypatch.setenv("DISKS_INTERVAL_SECONDS", "60")
monkeypatch.setenv("DISKS_TIMEOUT_SECONDS", "45")
reloaded = importlib.reload(config)
try:
assert reloaded.SETTINGS.disks_interval == 60.0
assert reloaded.SETTINGS.disks_timeout == 45.0
finally:
monkeypatch.delenv("DISKS_INTERVAL_SECONDS")
monkeypatch.delenv("DISKS_TIMEOUT_SECONDS")
importlib.reload(config)
def test_there_is_one_window_knob_per_measuring_cadence() -> None:
"""Phase 23: `RATE_WINDOW_SECONDS` is the **host's** Δ window; the LLM's *is* its interval.
These are the defaults `app.build_hub` spans its two estimators with — the wiring is pinned in
`tests/test_api.py`, the payload labels in `tests/test_llm_source.py` and
`tests/test_host_source.py`, both through `tests/rate_window_contract.py`. And there is
exactly **one** `*window*` setting: the LLM window tracks `LLM_INTERVAL_SECONDS` by
construction, so it cannot drift from the cadence it belongs to, while the host's cannot
shrink to match it either (a 2 s span over a 10 s poll has nothing inside it to diff, which
`test_the_host_rates_keep_their_own_cadence_s_window` pins as `n/a`).
"""
settings = Settings()
assert settings.rate_window == 10.0
assert settings.llm_interval == 2.0
assert settings.host_interval == 10.0
window_knobs = {field.name for field in fields(Settings) if "window" in field.name}
assert window_knobs == {"rate_window"}, (
"the LLM window must stay `LLM_INTERVAL_SECONDS`: a second knob drifts from its cadence")
def test_actions_need_the_flag_and_are_dead_under_read_only() -> None:
assert Settings(nut_allow_actions=True, read_only=False).actions_allowed is True
assert Settings(nut_allow_actions=True, read_only=True).actions_allowed is False
assert Settings(nut_allow_actions=False, read_only=False).actions_allowed is False
def test_nut_host_accepts_a_nut_scheme_prefix() -> None:
assert Settings(nut_host="nut://ups-box:3493").nut_enabled is True
def test_load_reads_host_targets_from_the_environment(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("HOST_TARGETS", "deskwork=http://tok@deskwork.example.net:9120")
settings = config.load()
assert [t.name for t in settings.host_targets] == ["deskwork"]
def test_env_driven_scalars_are_read_at_import(monkeypatch: pytest.MonkeyPatch) -> None:
"""The dataclass defaults read os.environ, so a reload must pick up a changed env."""
monkeypatch.setenv("DASH_PORT", "8123")
monkeypatch.setenv("RETAIN_RAW_HOURS", "6")
monkeypatch.setenv("NUT_ALLOW_ACTIONS", "1")
reloaded = importlib.reload(config)
try:
assert reloaded.SETTINGS.port == 8123
assert reloaded.SETTINGS.raw_keep_hours == 6
assert reloaded.SETTINGS.actions_allowed is True
finally:
monkeypatch.delenv("DASH_PORT")
monkeypatch.delenv("RETAIN_RAW_HOURS")
monkeypatch.delenv("NUT_ALLOW_ACTIONS")
importlib.reload(config)
def test_flags_accept_the_usual_spellings(monkeypatch: pytest.MonkeyPatch) -> None:
for truthy in ("1", "true", "TRUE", "yes", "on"):
monkeypatch.setenv("DASH_READ_ONLY", truthy)
assert config._flag("DASH_READ_ONLY") is True, truthy
for falsey in ("0", "false", "", "no"):
monkeypatch.setenv("DASH_READ_ONLY", falsey)
assert config._flag("DASH_READ_ONLY") is False, falsey
def test_numeric_env_must_be_a_number_or_the_default_wins(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("HOST_INTERVAL_SECONDS", "not-a-number")
assert config._num("HOST_INTERVAL_SECONDS", 10.0) == 10.0
monkeypatch.setenv("HOST_INTERVAL_SECONDS", "2.5")
assert config._num("HOST_INTERVAL_SECONDS", 10.0) == 2.5
def test_settings_is_immutable_so_nobody_mutates_shared_state() -> None:
settings: Any = Settings()
with pytest.raises(FrozenInstanceError):
settings.port = 1 # type: ignore[misc]
# ---- the .env convenience -------------------------------------------------- #
def test_no_env_file_is_the_normal_case(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
monkeypatch.setenv("DASH_ENV_FILE", str(tmp_path / "absent.env"))
assert config.load_env_file() is None
def test_env_file_seeds_values_a_deployed_env_does_not_set(monkeypatch: pytest.MonkeyPatch,
tmp_path: Any) -> None:
env = tmp_path / ".env"
env.write_text("DASH_PORT=9999\nNUT_HOST=ups.internal\n")
monkeypatch.setenv("DASH_ENV_FILE", str(env))
monkeypatch.delenv("DASH_PORT", raising=False)
monkeypatch.delenv("NUT_HOST", raising=False)
assert config.load_env_file() == env
# Settings reads its defaults when the module is imported, so the reload is what a real
# process gets for free and what the test has to ask for explicitly
reloaded = importlib.reload(config)
assert reloaded.SETTINGS.port == 9999
assert reloaded.SETTINGS.nut_host == "ups.internal"
def test_a_real_environment_variable_beats_the_file(monkeypatch: pytest.MonkeyPatch,
tmp_path: Any) -> None:
"""The container/quadlet env is the source of truth; the file is for laptops."""
env = tmp_path / ".env"
env.write_text("DASH_PORT=9999\n")
monkeypatch.setenv("DASH_ENV_FILE", str(env))
monkeypatch.setenv("DASH_PORT", "7777")
reloaded = importlib.reload(config)
assert os.environ["DASH_PORT"] == "7777"
assert reloaded.SETTINGS.port == 7777
def test_env_file_expands_other_variables(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
"""`.env.example` shows `DASH_DB=${HOME}/...` — the file has to mean what it shows."""
env = tmp_path / ".env"
env.write_text("MY_BASE=/srv/dash\nDASH_DB=${MY_BASE}/dashboards.sqlite3\n")
monkeypatch.setenv("DASH_ENV_FILE", str(env))
monkeypatch.delenv("MY_BASE", raising=False)
monkeypatch.delenv("DASH_DB", raising=False)
reloaded = importlib.reload(config)
assert reloaded.SETTINGS.db_path == "/srv/dash/dashboards.sqlite3"
def test_the_example_file_is_never_loaded(monkeypatch: pytest.MonkeyPatch, tmp_path: Any) -> None:
example = tmp_path / ".env.example"
example.write_text("NUT_HOST=should-not-load\n")
monkeypatch.setenv("DASH_ENV_FILE", str(example))
monkeypatch.delenv("NUT_HOST", raising=False)
reloaded = importlib.reload(config)
assert reloaded.load_env_file() is None
assert reloaded.SETTINGS.nut_host == ""
# ---- authentication settings (phase 11) ------------------------------------ #
def test_the_auth_defaults_are_a_locked_door_and_a_long_session() -> None:
"""No default password, a 30-day session, no `Secure` flag, OWASP's cost.
Each default is a decision, so each is asserted rather than inherited:
* `admin_password == ""` is what makes "no password configured" detectable at all — a
default of `testpass` would ship a known credential on every box that forgot the env.
* 720 h (30 days) because a dashboard that logs the operator out mid-outage gets bypassed.
* `cookie_secure` off: dev is `http://127.0.0.1`, and a default on would make the cookie
unsendable there (the task adds the `X-Forwarded-Proto` case for the real proxy).
* the cost is `auth.PBKDF2_ITERATIONS`, imported rather than repeated, so the primitive and
the setting cannot drift.
"""
settings = Settings()
assert settings.admin_password == ""
assert settings.session_hours == 720.0
assert settings.cookie_secure is False
assert settings.pbkdf2_iterations == PBKDF2_ITERATIONS == 600_000
def test_the_auth_settings_come_from_the_environment(monkeypatch: pytest.MonkeyPatch) -> None:
monkeypatch.setenv("DASH_ADMIN_PASSWORD", " spaced-but-real ")
monkeypatch.setenv("DASH_SESSION_HOURS", "12")
monkeypatch.setenv("DASH_COOKIE_SECURE", "1")
monkeypatch.setenv("DASH_PBKDF2_ITERATIONS", "4096")
reloaded = importlib.reload(config)
try:
assert reloaded.SETTINGS.admin_password == "spaced-but-real", "_env() strips"
assert reloaded.SETTINGS.session_hours == 12.0
assert reloaded.SETTINGS.cookie_secure is True
assert reloaded.SETTINGS.pbkdf2_iterations == 4096
finally:
for name in ("DASH_ADMIN_PASSWORD", "DASH_SESSION_HOURS", "DASH_COOKIE_SECURE",
"DASH_PBKDF2_ITERATIONS"):
monkeypatch.delenv(name)
importlib.reload(config)
def test_an_empty_DASH_ADMIN_PASSWORD_means_unset_not_blank(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""`DASH_ADMIN_PASSWORD=` is the operator saying "no seed", not "the password is empty".
The distinction is load-bearing: `load_auth_state` treats `""` as no seed and fails closed,
where a value of `""` that survived as a password would be the one credential shape an
attacker tries first.
"""
monkeypatch.setenv("DASH_ADMIN_PASSWORD", "")
reloaded = importlib.reload(config)
try:
assert reloaded.SETTINGS.admin_password == ""
finally:
monkeypatch.delenv("DASH_ADMIN_PASSWORD")
importlib.reload(config)
def test_the_pbkdf2_knob_is_for_tests_and_not_a_performance_tune(
monkeypatch: pytest.MonkeyPatch,
) -> None:
"""A garbage value falls back to the OWASP figure rather than to 0 or 1.
`_int`/`_num` answer a non-number with the default, so a typo in
`DASH_PBKDF2_ITERATIONS` cannot produce a verifier hashed once.
"""
monkeypatch.setenv("DASH_PBKDF2_ITERATIONS", "not-a-cost")
assert config._int("DASH_PBKDF2_ITERATIONS", 600_000) == 600_000
monkeypatch.setenv("DASH_PBKDF2_ITERATIONS", "1000")
assert config._int("DASH_PBKDF2_ITERATIONS", 600_000) == 1000