Files
chemenu/tools/chemenu/tests/test_config_shapes.py
T
torbenandClaude Opus 5.5 5994b2a88c
CI / verify (push) Successful in 8m8s
CI / pwsh (push) Successful in 2m40s
Release / release (push) Successful in 35s
Release / wiki-mirror (push) Skipped
raw: fetch behind a login - raw login stores the user's session as a secret outside the tree, fetch.routes sends a host through it or to an external fetcher, a handler's render goes through the optional browser component (preflight --with browser) (#169); 10.0.0-beta.8
Files changed:
- CHANGES.md
- INSTALL.md
- VERSION
- docs/how-to/README.md
- docs/how-to/fetch-pages-behind-a-login.md
- docs/how-to/ingest-a-source.md
- docs/reference/commands/README.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/raw-material-and-uploads.md
- docs/reference/configuration.md
- docs/reference/exit-codes.md
- instructions/preflight.md
- instructions/wiki-ingest/SKILL.md
- raw/CONTRACT.md
- sites/CONTRACT.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/browser.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/commands/raw/__init__.py
- tools/chemenu/commands/raw/fetch.py
- tools/chemenu/commands/raw/login.py
- tools/chemenu/config_shapes.py
- tools/chemenu/fetch_routes.py
- tools/chemenu/reference_doc.py
- tools/chemenu/settings.py
- tools/chemenu/sites/__init__.py
- tools/chemenu/sites/data.py
- tools/chemenu/sites/site.schema.json
- tools/chemenu/tests/test_cli.py
- tools/chemenu/tests/test_config_shapes.py
- tools/chemenu/tests/test_doctor.py
- tools/chemenu/tests/test_preflight.py
- tools/chemenu/tests/test_preflight_pwsh.py
- tools/chemenu/tests/test_raw_fetch_routes.py
- tools/chemenu/tests/test_raw_login.py
- tools/chemenu/tests/test_site_files.py
- tools/chemenu/web_capture.py
- tools/chemenu/web_session.py
- tools/preflight.ps1
- tools/preflight.sh
- tools/requirements-browser.txt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
2026-10-11 20:50:23 +02:00

308 lines
12 KiB
Python

"""`chemenu.config_shapes` - one record per section of the tiered settings,
which the loaders read and `docs reference` renders.
The loaders take their field names, required fields, defaults, coercions and
tiers from the record: a test changes the record and the loader follows.
"""
from __future__ import annotations
import ast
import dataclasses
import re
import sys
from pathlib import Path
import pytest
import typer
from typer.testing import CliRunner
from chemenu import blocks, config, config_shapes, reference_doc
from chemenu.cli import app
from chemenu.commands import docs_verify, git_publish
from chemenu.errors import ValidationError
from chemenu.tasks import caldav, superproductivity
from chemenu.tasks import config as tasks_config
from chemenu.telemetry import policy
from chemenu.tests.conftest import write_settings
runner = CliRunner()
PACKAGE = Path(config_shapes.__file__).parent
UPLOAD_OK = {
"identity_header": "X-User", "max_bytes": 1000, "allowed_extensions": [".md", ".PDF"],
"quota_submissions_per_day": 5, "quota_bytes_per_day": 5000,
}
TASKS_OK = {"provider": "caldav", "stalled_waiting_days": 14, "unpaged_project_weeks": 3,
"someday_stale_months": 5}
CALDAV_OK = {"url": "https://dav.example/", "username": "u", "app_password": "pw",
"inbox_list": "Inbox", "someday_list": "Someday"}
@pytest.fixture
def root(tmp_path, monkeypatch):
tree = tmp_path / "instance"
tree.mkdir()
monkeypatch.setattr(config, "ROOT", tree)
return tree
# --- the module itself -------------------------------------------------------
def test_the_module_imports_only_the_stdlib_and_chemenu_config():
"""A hook handler imports `chemenu.telemetry` on every tool call without
the venv, and the telemetry policy imports this module."""
tree = ast.parse(Path(config_shapes.__file__).read_text(encoding="utf-8"))
imported = set()
for node in ast.walk(tree):
if isinstance(node, ast.Import):
imported |= {alias.name for alias in node.names}
elif isinstance(node, ast.ImportFrom):
if node.module == "chemenu":
imported |= {f"chemenu.{alias.name}" for alias in node.names}
elif node.module != "__future__":
imported.add(node.module)
foreign = {
name for name in imported
if name != "chemenu.config" and name.split(".")[0] not in sys.stdlib_module_names
}
assert foreign == set()
def test_the_telemetry_policy_reads_the_record():
assert policy.config_shapes is config_shapes
assert policy.DEFAULT_MAX_SESSION_BYTES == config_shapes.TELEMETRY.field("max_session_bytes").default
assert policy.DEFAULT_KEEP_SESSIONS == config_shapes.TELEMETRY.field("keep_sessions").default
def test_one_record_per_section():
assert [shape.key for shape in config_shapes.SETTINGS_SECTIONS] == [
"telemetry", "remotes", "upload", "tasks", "superproductivity", "caldav", "sites", "fetch",
]
LOADERS = ("telemetry/policy.py", "commands/git_publish.py", "upload.py", "tasks/config.py",
"tasks/superproductivity.py", "tasks/caldav.py")
def test_no_hand_written_expected_text_is_left_in_a_loader():
pattern = re.compile(r"_EXPECTED_\w*\s*=|\bexpected\s*=\s*\(|Expected: \{\\?\"")
offenders = [
relative for relative in LOADERS
if pattern.search((PACKAGE / relative).read_text(encoding="utf-8"))
]
assert offenders == []
def test_the_known_names_follow_the_records():
assert tasks_config.KNOWN_PROVIDERS == ("superproductivity", "caldav")
assert superproductivity.KNOWN_ACCESS == ("api", "snapshot")
# --- the loaders take their expectations from the record --------------------
def _add_required(shape, name="added", tier=config_shapes.CHECKOUT):
return dataclasses.replace(
shape, fields=shape.fields + (config_shapes.Field(name, "string", "x", required=True, tier=tier),)
)
def _not_required(shape, name, default):
fields = tuple(
dataclasses.replace(f, required=False, default=default) if f.name == name else f
for f in shape.fields
)
return dataclasses.replace(shape, fields=fields)
def _swap(monkeypatch, attr, shape):
monkeypatch.setattr(config_shapes, attr, shape)
monkeypatch.setattr(
config_shapes, "SETTINGS_SECTIONS",
tuple(shape if s.key == shape.key else s for s in config_shapes.SETTINGS_SECTIONS),
)
def test_upload_follows_a_field_made_required_or_optional(root, monkeypatch):
from chemenu import upload
original = config_shapes.UPLOAD
write_settings(root, monkeypatch, upload=UPLOAD_OK)
assert upload.read_config(root).max_bytes == 1000
_swap(monkeypatch, "UPLOAD", _add_required(original))
with pytest.raises(ValidationError, match="added is missing"):
upload.read_config(root)
_swap(monkeypatch, "UPLOAD", original)
(root / config.LOCAL_SETTINGS_FILENAME).unlink()
(root / config.INSTANCE_SETTINGS_FILENAME).unlink()
write_settings(root, monkeypatch, upload={k: v for k, v in UPLOAD_OK.items() if k != "max_bytes"})
with pytest.raises(ValidationError):
upload.read_config(root)
_swap(monkeypatch, "UPLOAD", _not_required(original, "max_bytes", 99))
assert upload.read_config(root).max_bytes == 99
def test_tasks_follows_a_field_made_required(root, monkeypatch):
write_settings(root, monkeypatch, tasks=TASKS_OK, caldav=CALDAV_OK)
assert tasks_config.read_config(root) is not None
_swap(monkeypatch, "TASKS", _add_required(config_shapes.TASKS, tier=config_shapes.INSTANCE))
with pytest.raises(ValidationError, match="added is missing"):
tasks_config.read_config(root)
def test_remotes_follows_a_field_made_required(root, monkeypatch):
write_settings(root, monkeypatch, remotes={"allowed_push_urls": ["u"]})
assert git_publish.read_allowed_push_urls() == ["u"]
_swap(monkeypatch, "REMOTES", _add_required(config_shapes.REMOTES))
with pytest.raises(typer.Exit):
git_publish.read_allowed_push_urls()
def test_caldav_follows_a_field_made_required(monkeypatch):
from chemenu import settings
monkeypatch.setenv("WIKITOOL_SECRET_X", "pw")
data = dict(CALDAV_OK, app_password={"ref": "x"})
assert caldav.CalDAVConfig.from_section(
settings.Section.from_data(config_shapes.CALDAV, data)).exclude_lists == ()
fields = tuple(
dataclasses.replace(f, required=True) if f.name == "exclude_lists" else f
for f in config_shapes.CALDAV.fields
)
shape = dataclasses.replace(config_shapes.CALDAV, fields=fields)
with pytest.raises(ValidationError, match="exclude_lists is missing"):
settings.Section.from_data(shape, data)
def test_telemetry_follows_a_condition_in_the_record(root, monkeypatch):
write_settings(root, monkeypatch, telemetry={"keep_sessions": 500})
policy.reset_cache()
assert policy.resolve(root).keep_sessions == 500
shape = config_shapes.TELEMETRY
fields = tuple(
dataclasses.replace(f, check=lambda v: None if v < 100 else "too many") if f.name == "keep_sessions" else f
for f in shape.fields
)
_swap(monkeypatch, "TELEMETRY", dataclasses.replace(shape, fields=fields))
policy.reset_cache()
assert policy.resolve(root).keep_sessions == policy.DEFAULT_KEEP_SESSIONS
policy.reset_cache()
# --- deviations: what doctor reports for telemetry -------------------------
@pytest.mark.parametrize("data,field", [
({"enabled": "yes"}, "enabled"),
({"enabled": 1}, "enabled"),
({"max_session_bytes": 0}, "max_session_bytes"),
({"keep_sessions": "250"}, "keep_sessions"),
({"keep_sessions": 2.5}, "keep_sessions"),
({"enabled": True, "verbose": True}, "verbose"),
])
def test_a_telemetry_deviation_names_its_field(data, field):
found = config_shapes.deviations(config_shapes.TELEMETRY, data)
assert found and all(field in entry for entry in found)
def test_a_conforming_telemetry_file_has_no_deviation():
for example in config_shapes.TELEMETRY.examples:
assert config_shapes.deviations(config_shapes.TELEMETRY, example) == []
assert config_shapes.deviations(config_shapes.TELEMETRY, {"enabled": False}) == []
def test_every_example_passes_its_own_record():
from chemenu import settings
for shape in config_shapes.SETTINGS_SECTIONS:
for example in shape.examples:
settings.Section.from_data(shape, example)
# --- the generated regions --------------------------------------------------
def _page() -> str:
return (config.ROOT / reference_doc.CONFIGURATION).read_text(encoding="utf-8")
def _all_fields(shape):
for spec in shape.fields:
yield spec.name
def test_the_page_carries_one_region_per_file_and_every_field_in_it():
page = _page()
for name, attr in reference_doc.CONFIG_REGIONS:
shape = getattr(config_shapes, attr)
region = blocks.find(page, name)
assert region is not None, name
for path in _all_fields(shape):
assert f"`{path}`" in region, (name, path)
@pytest.mark.parametrize("relative", [reference_doc.CONFIGURATION, "docs/how-to/set-up-the-mcp-server.md"])
def test_no_json_example_for_these_files_stands_outside_a_region(relative):
text = blocks.mask_regions((config.ROOT / relative).read_text(encoding="utf-8"))
keys = {f.name for shape in config_shapes.SETTINGS_SECTIONS for f in shape.fields}
for block in re.findall(r"```json\n(.*?)```", text, re.S):
assert not any(f'"{key}"' in block for key in keys), block
def _only_config_regions(monkeypatch):
real = reference_doc.regions
monkeypatch.setattr(
reference_doc, "regions",
lambda *a, **k: [r for r in real(*a, **k) if r.path == reference_doc.CONFIGURATION],
)
def _hand_written(text: str) -> str:
for name, _ in reference_doc.CONFIG_REGIONS:
text = re.sub(
re.escape(blocks.open_marker(name)) + ".*?" + re.escape(blocks.close_marker(name)),
blocks.open_marker(name), text, flags=re.S,
)
return text
def test_apply_fills_every_region_of_the_page_and_a_second_run_changes_nothing(tmp_path, monkeypatch):
"""Four regions in one file: each is refreshed into the text the one
before it left, or only the last would survive."""
target = tmp_path / reference_doc.CONFIGURATION
target.parent.mkdir(parents=True)
real = _page()
emptied = real
for name, _ in reference_doc.CONFIG_REGIONS:
emptied = blocks.replace(emptied, name, blocks.open_marker(name) + "\n" + blocks.close_marker(name) + "\n")
target.write_text(emptied, encoding="utf-8")
monkeypatch.setattr(config, "ROOT", tmp_path)
_only_config_regions(monkeypatch)
result = runner.invoke(app, ["docs", "reference", "--apply"])
assert result.exit_code == 0, result.output
applied = target.read_text(encoding="utf-8")
assert applied == real
assert _hand_written(applied) == _hand_written(emptied)
result = runner.invoke(app, ["docs", "reference", "--apply"])
assert "already current" in result.output
assert target.read_text(encoding="utf-8") == applied
def test_a_changed_record_without_apply_fails_docs_verify(monkeypatch):
assert not [i for i in docs_verify.check_reference_regions() if "configuration.md" in i]
_swap(monkeypatch, "REMOTES", _add_required(config_shapes.REMOTES))
issues = [i for i in docs_verify.check_reference_regions() if reference_doc.CONFIGURATION in i]
assert len(issues) == 1
assert "reference-config-remotes" in issues[0] and "docs reference --apply" in issues[0]
def test_the_configuration_regions_are_checked_in_an_instance_too():
regions = [r for r in reference_doc.regions() if r.path == reference_doc.CONFIGURATION]
assert [r.name for r in regions] == [name for name, _ in reference_doc.CONFIG_REGIONS]
assert not any(r.origin_only for r in regions)