The shipped config could not load on a machine without a .env

config/default.yaml reads its camera's host, username and password from
${ENV}. Unset placeholders parse as YAML null, and CameraConfig had no
_normalize_blanks - the guard ApiSection and EmailSection have had all
along - so loading it raised three pydantic errors and the engine would
not start at all. On a developer's checkout .env is right there, which is
why this survived: the failing machine is every machine the product is
actually installed on, and installer/build.ps1 runs this suite, so the
Windows build would have failed on a fresh clone.

The normalisation is field-by-field, never a blanket None -> "": `webcam`
is an Optional[int] whose None means "this is not a webcam", and `tuning`
is a nested model. Sweeping either trades one validation error for
another - which it did, on the first attempt.

Second bug behind the same line: that camera entry would then have been
SEEDED into a fresh install, giving a shop a camera called cam1 that
nobody added, retrying a connection to "" forever, with the first task on
a new PC being to work out what it was. CameraConfig.addressed() says
what a camera entry needs to be one, and seed() drops the rest.

Found by cloning the repository into a temp directory and running the
tests there. Nothing in a working tree can find this class of bug.

Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01HViLj9gYNRtSr7YVZmW5sn
This commit is contained in:
2026-09-04 12:15:53 +05:30
parent 2b69a0be3d
commit c7024b57ca
3 changed files with 110 additions and 3 deletions

View File

@@ -1,6 +1,8 @@
"""The shipped YAML carries thresholds measured on the deployment site; the
pydantic defaults must not drift away from them (a trimmed config would then
silently re-admit the false positives those values were tuned to reject)."""
from pathlib import Path
import pytest
from behavision.config import (ApiSection, Config, DetectionSection,
@@ -123,3 +125,62 @@ def test_no_usable_model_fails_loudly(tmp_path):
(tmp_path / "w600k_mbf.onnx").write_bytes(b"garbage")
with pytest.raises(FileNotFoundError, match="no usable recognition model"):
ArcFaceEncoder(tmp_path)
def _no_env(monkeypatch, tmp_path):
"""Make the process look like an installed PC rather than a checkout.
`load_config` calls `load_dotenv` first, so simply unsetting the variables
is not enough in a working tree - the .env sitting next to the code puts
them straight back. Removing the .env from the picture is the condition
these tests are actually about.
"""
import behavision.paths as paths
monkeypatch.setattr(paths, "env_file", lambda: None)
for var in ("BEHAVISION_CAM1_HOST", "BEHAVISION_CAM1_USERNAME",
"BEHAVISION_CAM1_PASSWORD", "BEHAVISION_API_USER",
"BEHAVISION_API_PASSWORD", "BEHAVISION_WEBHOOK_URL"):
monkeypatch.delenv(var, raising=False)
monkeypatch.setenv("BEHAVISION_DATA_DIR", str(tmp_path))
def test_the_bundled_config_loads_with_no_environment_at_all(tmp_path, monkeypatch):
"""What a freshly installed PC looks like: config/default.yaml, and nothing
else.
Every `${VAR}` in that file then resolves to YAML null. The engine's own
sections already guarded against that; the camera section did not, so the
shipped config raised three pydantic errors on host, username and password
and the engine would not start at all on any machine without a developer's
.env - which is every machine the product is installed on. It was found by
cloning the repository and running this suite, never by working in a
checkout where .env is right there.
"""
_no_env(monkeypatch, tmp_path)
root = Path(__file__).resolve().parent.parent
cfg = load_config(root / "config" / "default.yaml")
assert cfg.api.username == ""
assert cfg.api.port == 8010
for cam in cfg.cameras:
assert cam.host == "" and cam.username == "" and cam.password == ""
assert cam.port == 554
def test_a_camera_with_no_address_is_not_seeded(tmp_path, monkeypatch):
"""The bundled config declares a camera whose address comes from the
environment. On an installed PC there is none, so seeding it would put a
camera the shop never added into a brand new install, permanently failing
to connect to "" - and their first task would be working out what it was."""
from behavision.cameras import CameraStore
_no_env(monkeypatch, tmp_path)
root = Path(__file__).resolve().parent.parent
cfg = load_config(root / "config" / "default.yaml")
store = CameraStore(tmp_path / "cameras.json")
assert store.seed(cfg.cameras) is False
assert store.list() == []
assert not (tmp_path / "cameras.json").exists()