Eric spotted it: the README told people to clone this repo, rm the committed data, and run — which writes THEIR pipeline artifacts at git-TRACKED paths. The next `git pull` (this repo commits data every session) refuses to merge, and the internet's standard remedies for that error — reset --hard, checkout ., stash, clean -fdx — destroy their review decisions, hand-written games, upload log, and photos. Two layers. The README's "Bring your own shelves" now leads with `uv tool install git+…` and running in a directory of your own: data lands untracked by construction and a bug fix is `uv tool upgrade`, which cannot touch it. And because nobody re-reads a README, Config gains tracked_data_warning(): if artifacts under data_dir are git-tracked, `bggpipe init` and the web dashboard both warn in plain words. The owner's exemption is data/.own_repo — a GITIGNORED marker, so the author's checkout is silent while a fresh clone of the same repo still gets the warning (a committed marker or config key would have shipped the exemption to exactly the people who need warning). Co-Authored-By: Claude Fable 5 <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_016jXZFSTZQKzAC8fqpWSz9g
141 lines
4.4 KiB
Python
141 lines
4.4 KiB
Python
"""Config loading: toml knobs, env-only username, unknown-key warning."""
|
|
|
|
from __future__ import annotations
|
|
|
|
import os
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from bggpipe.config import Config, load_config
|
|
|
|
|
|
def test_defaults_when_no_file(tmp_path, monkeypatch):
|
|
# no config.toml in cwd and no explicit path -> defaults (an EXPLICIT
|
|
# missing path errors instead; see the dedicated test)
|
|
monkeypatch.delenv("BGG_USERNAME", raising=False)
|
|
monkeypatch.chdir(tmp_path)
|
|
cfg = load_config()
|
|
assert cfg == Config()
|
|
assert cfg.cache_dir == Path("data/bgg_cache")
|
|
|
|
|
|
def test_reads_toml(tmp_path, monkeypatch):
|
|
monkeypatch.delenv("BGG_USERNAME", raising=False)
|
|
p = tmp_path / "config.toml"
|
|
p.write_text('data_dir = "elsewhere"\nrate_limit_seconds = 3\n')
|
|
cfg = load_config(p)
|
|
assert cfg.data_dir == Path("elsewhere")
|
|
assert cfg.rate_limit_seconds == 3.0
|
|
assert cfg.matches_path == Path("elsewhere/matches.csv")
|
|
|
|
|
|
def test_username_comes_from_env_only(tmp_path, monkeypatch):
|
|
# the account has exactly one home: BGG_USERNAME in the environment —
|
|
# a bgg_username key in config.toml is deliberately ignored
|
|
p = tmp_path / "config.toml"
|
|
p.write_text('bgg_username = "from_toml"\n')
|
|
monkeypatch.setenv("BGG_USERNAME", "from_env")
|
|
assert load_config(p).bgg_username == "from_env"
|
|
monkeypatch.delenv("BGG_USERNAME")
|
|
assert load_config(p).bgg_username == ""
|
|
|
|
|
|
def test_unknown_toml_keys_warn(tmp_path, monkeypatch):
|
|
# a typo'd knob must not silently fall back to defaults
|
|
monkeypatch.delenv("BGG_USERNAME", raising=False)
|
|
p = tmp_path / "config.toml"
|
|
p.write_text('photo_dir = "oops"\n')
|
|
with pytest.warns(UserWarning, match="photo_dir"):
|
|
load_config(p)
|
|
|
|
|
|
def test_explicit_missing_config_errors_instead_of_silent_defaults(tmp_path):
|
|
with pytest.raises(FileNotFoundError, match="does not exist"):
|
|
load_config(tmp_path / "nope.toml")
|
|
|
|
|
|
def test_vision_blocks_apply_only_for_the_active_provider(tmp_path):
|
|
p = tmp_path / "config.toml"
|
|
p.write_text(
|
|
"""
|
|
vision_provider = "openai-compatible"
|
|
|
|
[vision.anthropic]
|
|
model = "claude-sonnet-5"
|
|
|
|
[vision."openai-compatible"]
|
|
base_url = "http://localhost:11434/v1"
|
|
model = "qwen2.5vl:7b"
|
|
key_env = ""
|
|
"""
|
|
)
|
|
cfg = load_config(p)
|
|
assert cfg.model == "qwen2.5vl:7b" # the active block's model wins
|
|
assert cfg.vision_base_url == "http://localhost:11434/v1"
|
|
assert cfg.vision_key_env == ""
|
|
# flip the provider: the other block applies, this one is inert
|
|
p.write_text(p.read_text().replace('"openai-compatible"\n', '"anthropic"\n', 1))
|
|
cfg = load_config(p)
|
|
assert cfg.model == "claude-sonnet-5"
|
|
assert cfg.vision_base_url == "" # untouched default
|
|
|
|
|
|
def test_vision_block_typos_warn(tmp_path):
|
|
p = tmp_path / "config.toml"
|
|
p.write_text(
|
|
"""
|
|
[vision.anthropic]
|
|
modle = "oops"
|
|
|
|
[vision.anthorpic]
|
|
model = "claude-sonnet-5"
|
|
"""
|
|
)
|
|
with (
|
|
pytest.warns(UserWarning, match="modle"),
|
|
pytest.warns(UserWarning, match="anthorpic"),
|
|
):
|
|
load_config(p)
|
|
|
|
|
|
def _git(tmp_path, *args):
|
|
import subprocess
|
|
|
|
subprocess.run(
|
|
["git", *args],
|
|
cwd=tmp_path,
|
|
check=True,
|
|
capture_output=True,
|
|
env={
|
|
"PATH": os.environ["PATH"],
|
|
"HOME": str(tmp_path),
|
|
"GIT_AUTHOR_NAME": "t",
|
|
"GIT_AUTHOR_EMAIL": "t@t",
|
|
"GIT_COMMITTER_NAME": "t",
|
|
"GIT_COMMITTER_EMAIL": "t@t",
|
|
},
|
|
)
|
|
|
|
|
|
def test_tracked_data_warning_fires_only_inside_a_repo(tmp_path):
|
|
"""The clone-and-run trap: data at git-tracked paths is one panicked
|
|
`git reset --hard` from destruction. Outside a repo, silence."""
|
|
data = tmp_path / "data"
|
|
data.mkdir()
|
|
(data / "matches.csv").write_text("x")
|
|
cfg = Config(data_dir=data)
|
|
assert cfg.tracked_data_warning() is None # no repo, no trap
|
|
|
|
_git(tmp_path, "init")
|
|
assert cfg.tracked_data_warning() is None # repo, but nothing tracked
|
|
_git(tmp_path, "add", "data/matches.csv")
|
|
_git(tmp_path, "commit", "-m", "seed")
|
|
warning = cfg.tracked_data_warning()
|
|
assert warning and "git-TRACKED" in warning and ".own_repo" in warning
|
|
|
|
# the owner's exemption is a gitignored marker: a fresh clone of this
|
|
# repo would NOT carry it, so only the author's checkout is silenced
|
|
(data / ".own_repo").touch()
|
|
assert cfg.tracked_data_warning() is None
|