mirror of
https://github.com/pewdiepie-archdaemon/odysseus.git
synced 2026-10-06 23:12:22 +02:00
The configuration surface was undiscoverable. .env.example has three active
lines, and of the ODYSSEUS_* variables the code actually reads, most appear
nowhere in .env.example, docs/, website/ or README.md - including several that
change security-relevant behaviour (ODYSSEUS_BROWSER_NO_SANDBOX,
ODYSSEUS_ALLOW_PRIVATE_CALDAV, ODYSSEUS_ENABLE_HOST_DOCKER,
ODYSSEUS_MCP_ALLOWED_COMMANDS). Every question about one of them lands in the
issue tracker.
A hand-written page would drift within a month, so the page is generated:
- scripts/generate_env_reference.py walks the Python sources, collects each read
with the default it falls back to and the file it is read in, groups by area,
and marks the internal variables rather than omitting them.
- website/configuration-reference.md is the generated output, wired into the
Pages layout and linked from setup.md and .env.example. .env.example stays a
short deployment-level example and links onward rather than growing.
- tests/test_env_reference.py regenerates and compares, so adding a variable
without documenting it fails the suite. That is the point: the current state
happened because nothing objected.
Finding the reads needs more than one pattern. Two families - the upload caps in
src/upload_limits.py and the media-ingress overrides in src/media_ingress.py -
are read through helper functions, so the generator detects env-reader helpers
rather than hardcoding a list. Others span two lines, hold the variable name in
a module constant, or read through a mapping passed in as an argument. One lives
inside a string literal, in the Ollama probe script routes/cookbook_helpers.py
builds line by line. A line-based grep for os.environ.get("ODYSSEUS_ finds 70 of
the 98 the generator finds; the page reports that gap and recomputes it on every
run so the claim cannot go stale.
No application behaviour changes.
224 lines
7.3 KiB
Python
224 lines
7.3 KiB
Python
"""Guards for the generated ODYSSEUS_* configuration reference.
|
|
|
|
`website/configuration-reference.md` is produced by
|
|
`scripts/generate_env_reference.py`. The point of these tests is that adding a
|
|
new `ODYSSEUS_*` read without documenting it fails the suite: the current state -
|
|
most of the configuration surface undiscoverable - happened because nothing
|
|
objected.
|
|
|
|
The generator's detection is exercised against a synthetic source tree rather
|
|
than against the real one, so a test failure names a behavior rather than a
|
|
count that drifted. Only the two whole-tree tests touch the repository, and they
|
|
compare generator output to the committed page instead of asserting on source
|
|
text.
|
|
"""
|
|
import re
|
|
import subprocess
|
|
import sys
|
|
from pathlib import Path
|
|
|
|
import pytest
|
|
|
|
from tests.helpers.cli_loader import load_script
|
|
|
|
REPO = Path(__file__).resolve().parent.parent
|
|
PAGE = REPO / "website" / "configuration-reference.md"
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def generator():
|
|
return load_script("generate_env_reference.py")
|
|
|
|
|
|
@pytest.fixture(scope="module")
|
|
def built(generator):
|
|
"""The generator run once against the real tree; reused by the slow tests."""
|
|
return generator.build()
|
|
|
|
|
|
def _write(root: Path, relative: str, body: str) -> None:
|
|
path = root / relative
|
|
path.parent.mkdir(parents=True, exist_ok=True)
|
|
path.write_text(body, encoding="utf-8")
|
|
|
|
|
|
@pytest.fixture
|
|
def fake_tree(tmp_path, generator, monkeypatch):
|
|
"""A miniature source tree covering every read pattern the generator claims."""
|
|
monkeypatch.setattr(generator, "SOURCE_ROOTS", ("app.py", "src"))
|
|
_write(tmp_path, "app.py", """
|
|
import os
|
|
|
|
DIRECT = os.environ.get("ODYSSEUS_DIRECT_GET", "on")
|
|
GETENV = os.getenv("ODYSSEUS_PLAIN_GETENV")
|
|
SUBSCRIPT = os.environ["ODYSSEUS_SUBSCRIPT"]
|
|
SPANNING = os.environ.get(
|
|
"ODYSSEUS_SPANS_TWO_LINES", "spanned"
|
|
)
|
|
os.environ["ODYSSEUS_WRITE_ONLY"] = "1"
|
|
""")
|
|
_write(tmp_path, "src/indirect.py", '''
|
|
import os
|
|
|
|
NAME_HELD_IN_CONSTANT = "ODYSSEUS_VIA_CONSTANT"
|
|
FALLBACK = 7
|
|
|
|
value = os.environ.get(NAME_HELD_IN_CONSTANT, FALLBACK)
|
|
|
|
|
|
def read_limit(name, default):
|
|
"""An env-reader helper: the generator should follow calls to this."""
|
|
raw = os.getenv(name)
|
|
return default if raw is None else int(raw)
|
|
|
|
|
|
LIMIT = read_limit("ODYSSEUS_VIA_HELPER", 5 * 1024)
|
|
|
|
|
|
def flag(environ=None):
|
|
source = os.environ if environ is None else environ
|
|
return source.get("ODYSSEUS_VIA_MAPPING_ARG", "1")
|
|
|
|
|
|
GENERATED = [
|
|
"import os",
|
|
"if os.environ.get('ODYSSEUS_INSIDE_A_STRING'): pass",
|
|
]
|
|
''')
|
|
return tmp_path
|
|
|
|
|
|
def test_finds_every_read_pattern_it_claims_to(generator, fake_tree):
|
|
found = generator.collect(fake_tree)
|
|
|
|
assert set(found) == {
|
|
"ODYSSEUS_DIRECT_GET",
|
|
"ODYSSEUS_PLAIN_GETENV",
|
|
"ODYSSEUS_SUBSCRIPT",
|
|
"ODYSSEUS_SPANS_TWO_LINES",
|
|
"ODYSSEUS_VIA_CONSTANT",
|
|
"ODYSSEUS_VIA_HELPER",
|
|
"ODYSSEUS_VIA_MAPPING_ARG",
|
|
"ODYSSEUS_INSIDE_A_STRING",
|
|
}
|
|
|
|
|
|
def test_a_plain_grep_would_miss_what_the_extra_passes_find(generator, fake_tree):
|
|
"""Pins why the generator is not a one-line grep."""
|
|
naive = generator.naive_line_scan(fake_tree)
|
|
found = set(generator.collect(fake_tree))
|
|
|
|
assert found - naive == {
|
|
"ODYSSEUS_SPANS_TWO_LINES",
|
|
"ODYSSEUS_VIA_CONSTANT",
|
|
"ODYSSEUS_VIA_HELPER",
|
|
"ODYSSEUS_VIA_MAPPING_ARG",
|
|
}
|
|
# And it over-counts in the other direction: a line-based scan cannot tell a
|
|
# write from a read, which is why the page reports the intersection.
|
|
assert naive - found == {"ODYSSEUS_WRITE_ONLY"}
|
|
|
|
|
|
def test_records_defaults_and_locations_from_the_source(generator, fake_tree):
|
|
found = generator.collect(fake_tree)
|
|
|
|
assert found["ODYSSEUS_DIRECT_GET"].primary.default == "'on'"
|
|
assert found["ODYSSEUS_SPANS_TWO_LINES"].primary.default == "'spanned'"
|
|
# One level of indirection is resolved: the name and the default both come
|
|
# from module-level constants.
|
|
assert found["ODYSSEUS_VIA_CONSTANT"].primary.default == "7"
|
|
assert found["ODYSSEUS_VIA_HELPER"].primary.default == "5 * 1024"
|
|
assert found["ODYSSEUS_PLAIN_GETENV"].primary.default is None
|
|
|
|
assert found["ODYSSEUS_DIRECT_GET"].primary.location == "app.py:4"
|
|
assert found["ODYSSEUS_VIA_HELPER"].primary.path == "src/indirect.py"
|
|
|
|
|
|
def test_environ_writes_are_not_reads(generator, fake_tree):
|
|
assert "ODYSSEUS_WRITE_ONLY" not in generator.collect(fake_tree)
|
|
|
|
|
|
def test_an_undocumented_variable_is_reported(generator, fake_tree):
|
|
"""The whole point: a new variable with no notes entry must fail loudly."""
|
|
problems = generator.check_notes(generator.collect(fake_tree))
|
|
|
|
assert problems
|
|
offender = "ODYSSEUS_VIA_HELPER"
|
|
assert any(offender in problem for problem in problems)
|
|
assert any("VARIABLE_NOTES" in problem for problem in problems)
|
|
assert any("src/indirect.py" in problem for problem in problems)
|
|
|
|
|
|
def test_a_stale_notes_entry_is_reported(generator):
|
|
problems = generator.check_notes({})
|
|
|
|
assert len(problems) == len(generator.VARIABLE_NOTES)
|
|
assert all("no longer read anywhere" in problem for problem in problems)
|
|
|
|
|
|
def test_renders_one_table_row_per_variable(generator, fake_tree, monkeypatch):
|
|
monkeypatch.setitem(
|
|
generator.VARIABLE_NOTES,
|
|
"ODYSSEUS_VIA_HELPER",
|
|
("Search", generator.USER, "A synthetic limit."),
|
|
)
|
|
found = {"ODYSSEUS_VIA_HELPER": generator.collect(fake_tree)["ODYSSEUS_VIA_HELPER"]}
|
|
|
|
page = generator.render(found, generator.naive_line_scan(fake_tree))
|
|
|
|
assert page.startswith("---\nlayout: default\n---\n")
|
|
assert "| `ODYSSEUS_VIA_HELPER` | `5 * 1024` | `src/indirect.py:16` | A synthetic limit. |" in page
|
|
assert "### Search" in page
|
|
|
|
|
|
def test_every_variable_read_in_the_repository_is_documented(built):
|
|
_, variables, problems = built
|
|
|
|
assert not problems, "\n".join(problems)
|
|
assert variables, "expected the generator to find ODYSSEUS_* reads"
|
|
|
|
|
|
def test_committed_page_matches_the_source(built):
|
|
page, _, _ = built
|
|
|
|
assert PAGE.read_text(encoding="utf-8") == page, (
|
|
"website/configuration-reference.md is stale - regenerate it with "
|
|
"`python3 scripts/generate_env_reference.py`"
|
|
)
|
|
|
|
|
|
def test_page_has_no_emoji_or_other_non_ascii(built):
|
|
page, _, _ = built
|
|
|
|
offenders = sorted({character for character in page if ord(character) > 126})
|
|
assert not offenders, f"non-ASCII in the generated page: {offenders}"
|
|
|
|
|
|
def test_check_mode_reports_a_stale_page(generator, tmp_path, monkeypatch):
|
|
stale = tmp_path / "configuration-reference.md"
|
|
stale.write_text("out of date\n", encoding="utf-8")
|
|
monkeypatch.setattr(generator, "OUTPUT_PATH", stale)
|
|
|
|
assert generator.main(["--check"]) == 1
|
|
assert generator.main([]) == 0
|
|
assert generator.main(["--check"]) == 0
|
|
|
|
|
|
def test_script_runs_as_a_subprocess_without_importing_the_app():
|
|
result = subprocess.run(
|
|
[sys.executable, "scripts/generate_env_reference.py", "--check"],
|
|
cwd=REPO, capture_output=True, text=True, timeout=180,
|
|
)
|
|
|
|
assert result.returncode == 0, result.stderr
|
|
assert re.search(r"\d+ variables", result.stdout), result.stdout
|
|
|
|
|
|
def test_page_is_linked_from_the_places_a_reader_starts():
|
|
assert "configuration-reference.md" in (REPO / "website" / "setup.md").read_text(
|
|
encoding="utf-8"
|
|
)
|
|
assert "configuration-reference.md" in (REPO / ".env.example").read_text(
|
|
encoding="utf-8"
|
|
)
|