settings: three tiers - wikitool.json (instance), .wikitool-local.json (checkout), secrets by reference incl. 1Password CLI; new command config set (#207)
CI / verify (push) Successful in 8m5s
CI / pwsh (push) Successful in 2m34s
Release / release (push) Successful in 36s
Release / wiki-mirror (push) Skipped

Every settings field declares its tier; a value in the wrong file is an error naming
file, key and tier. A field naming a program or affecting a gate cannot be declared
instance. Secrets are references only: {"ref": "<name>"} via WIKITOOL_SECRET_<NAME> or
a user-only secrets file, {"ref": "op://..."} via op read (locked 1Password: exit 42).
The preflight records op as an optional tool. doctor gains one line per tier, docs
verify checks a committed wikitool.json for plaintext secrets, CI resolves a real
op:// value through 1password/load-secrets-action. No feature reads its settings
this way yet; without the two files every other command is unchanged.

Files changed:
- .gitea/workflows/ci.yml
- .gitignore
- AGENTS.md
- CHANGES.md
- INSTALL.md
- VERSION
- docs/explanation/README.md
- docs/explanation/configuration-tiers.md
- docs/how-to/README.md
- docs/how-to/use-1password-for-settings.md
- docs/reference/commands/README.md
- docs/reference/commands/configuration.md
- docs/reference/commands/instance-health.md
- docs/reference/commands/types-instructions-and-docs.md
- docs/reference/configuration.md
- docs/reference/exit-codes.md
- instructions/dev/doc-pull-through.md
- instructions/dev/testing-conventions.md
- instructions/preflight.md
- tools/CONTRACT.md
- tools/README.md
- tools/chemenu/cli.py
- tools/chemenu/cli_contract.py
- tools/chemenu/commands/_util.py
- tools/chemenu/commands/config_cmd.py
- tools/chemenu/commands/docs_verify.py
- tools/chemenu/commands/doctor.py
- tools/chemenu/config.py
- tools/chemenu/config_shapes.py
- tools/chemenu/prerequisites.py
- tools/chemenu/settings.py
- tools/chemenu/tests/conftest.py
- tools/chemenu/tests/test_dist_cmd.py
- tools/chemenu/tests/test_preflight.py
- tools/chemenu/tests/test_preflight_pwsh.py
- tools/chemenu/tests/test_settings.py
- tools/chemenu/toolpaths.py
- tools/preflight.ps1
- tools/preflight.sh
- tools/prerequisites.txt

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SnAJ7Z3CpVD3PRbN73QtU2
This commit is contained in:
torbenandClaude Opus 5.5 committed 2026-10-10 15:34:28 +02:00
1 parent 059a0a1840
commit f6aaadc413
40 files changed
+2246 -31

No files matched your search

+4
View File
@@ -160,6 +160,10 @@ def hermetic_environment(tmp_path: Path, monkeypatch: pytest.MonkeyPatch):
for name in (*_WIKITOOL_ENV, *_GIT_ENV):
monkeypatch.delenv(name, raising=False)
# A family rather than a name: `{"ref": "<name>"}` reads
# `WIKITOOL_SECRET_<NAME>` for any name, so every one is cleared.
for name in [n for n in os.environ if n.startswith(config.ENV_SECRET_PREFIX)]:
monkeypatch.delenv(name, raising=False)
# `.wikitool-tools.json` beside this checkout records where the developer's
# git and rg live. A test must not depend on whether the preflight has run
+13
View File
@@ -202,6 +202,19 @@ def test_plan_ships_no_retired_root_docs(repo):
assert "README.md" in plan
def test_plan_ships_neither_settings_file(repo):
"""`wikitool.json` belongs to the instance that wrote it - its id, its
references - and `.wikitool-local.json` to one checkout. A distribution
carries neither; the first `config set` in the new instance creates its own."""
(repo / config.INSTANCE_SETTINGS_FILENAME).write_text(
'{"schema": 1, "instance_id": "origin-only"}\n', encoding="utf-8")
(repo / config.LOCAL_SETTINGS_FILENAME).write_text('{"schema": 1}\n', encoding="utf-8")
plan = dist_cmd.build_plan()
assert config.INSTANCE_SETTINGS_FILENAME not in plan
assert config.LOCAL_SETTINGS_FILENAME not in plan
assert "README.md" in plan
def test_plan_never_includes_instructions_dev(repo):
"""instructions/dev/ - flat dev-only instructions and the nested skill
that switches a session into tool-development mode - is pruned
+34
View File
@@ -185,6 +185,40 @@ def test_complete_path_records_absolute_paths_and_sets_up_the_venv(machine, shel
assert "Preflight passed" in result.stdout
@pytest.mark.parametrize("shell", SHELLS)
def test_an_optional_op_is_recorded_when_found(machine, shell):
machine.standard()
machine.stub("op", "#!/bin/sh\necho '2.30.0'\n")
result = machine.run(shell=shell)
assert result.returncode == 0, result.stdout + result.stderr
assert machine.recorded()["tools"]["op"] == str(machine.stubs / "op")
assert any(line.split()[:3] == ["OK", "op", "2.30"] for line in result.stdout.splitlines())
@pytest.mark.parametrize("shell", SHELLS)
def test_an_absent_optional_op_is_no_problem_and_not_recorded(machine, shell):
machine.standard()
result = machine.run(shell=shell)
assert result.returncode == 0, result.stdout + result.stderr
assert "op" not in machine.recorded()["tools"]
assert any(line.split()[:2] == ["ABSENT", "op"] for line in result.stdout.splitlines())
@pytest.mark.parametrize("shell", SHELLS)
def test_set_names_an_optional_tool(machine, shell):
machine.standard()
elsewhere = machine.stub("op", "#!/bin/sh\necho '2.30.0'\n", where=machine.base / "elsewhere")
result = machine.run("--set", f"op={elsewhere}", shell=shell)
assert result.returncode == 0, result.stdout + result.stderr
assert machine.recorded()["tools"]["op"] == str(elsewhere)
def test_the_manifest_names_op_as_optional_only():
manifest = prerequisites.load_manifest()
assert [tool.name for tool in manifest.optional] == ["op"]
assert "op" not in {tool.name for tool in manifest.tools}
@pytest.mark.parametrize("shell", SHELLS)
def test_second_run_changes_nothing(machine, shell):
machine.standard()
@@ -95,6 +95,24 @@ def test_both_preflights_record_the_same_file(machine):
assert machine.tools_file.read_bytes() == from_pwsh
@pytest.mark.skipif(not SHELLS, reason="no POSIX shell on this machine")
def test_both_preflights_record_an_optional_op_the_same_way(machine):
machine.stub("op", "#!/bin/sh\necho '2.30.0'\n")
assert _preflight(machine).returncode == 0
from_pwsh = machine.tools_file.read_bytes()
assert json.loads(from_pwsh)["tools"]["op"] == str(machine.stubs / "op")
machine.tools_file.unlink()
assert machine.run(shell=SHELLS[0]).returncode == 0
assert machine.tools_file.read_bytes() == from_pwsh
def test_an_absent_optional_op_is_no_problem(machine):
result = _preflight(machine)
assert result.returncode == 0, result.stdout + result.stderr
assert "op" not in machine.recorded()["tools"]
assert any(line.split()[:2] == ["ABSENT", "op"] for line in result.stdout.splitlines())
def test_help_prints_the_usage_and_exits_0(machine):
result = _preflight(machine, "--help")
assert result.returncode == 0
+574
View File
@@ -0,0 +1,574 @@
"""The tiered settings (`chemenu.settings`), `wikitool config set`, and what
`doctor` and `docs verify` report about them.
No real section ships yet, so every test stands in `TRACKER`, a section with one
field of each kind: an instance value, a private checkout value, a secret, a
field that names a program, and a plain checkout value. 1Password is a fake `op`
script written into the test's own directory - never a real account; the one
exception is the CI smoke test at the bottom, which only runs where CI has
loaded a real `op://` value into the environment.
"""
from __future__ import annotations
import json
import os
import shlex
import stat
import subprocess
from pathlib import Path
import pytest
import typer
from typer.testing import CliRunner
from chemenu import config, config_shapes, settings, toolpaths
from chemenu.cli import app
from chemenu.commands import _util, docs_verify, doctor
from chemenu.config_shapes import CHECKOUT, INSTANCE, SECRET, Field, Shape
from chemenu.errors import ValidationError
runner = CliRunner()
# Captured at import, before the autouse fixture clears every
# `WIKITOOL_SECRET_*` for the test run - see the CI smoke test at the bottom.
_LIVE_VALUE = os.environ.get("WIKITOOL_SECRET_CI_SMOKE", "")
_LIVE_REQUIRED = os.environ.get("CHEMENU_LIVE_SECRET_REQUIRE", "") == "1"
needs_posix = pytest.mark.skipif(os.name == "nt", reason="the fake `op` is a POSIX shell script")
SECRET_VALUE = "s3cr3t-never-printed"
PRIVATE_VALUE = "https://tracker.internal.example"
def _positive(value: int):
return None if value > 0 else "must be positive"
TRACKER = Shape(
key="tracker", title="Test tracker", purpose="A section with one field of each kind.",
fields=(
Field("stale_days", "positive integer", "Instance threshold.", tier=INSTANCE,
json_type="integer", coerce=int, check=_positive, default=14),
Field("url", "string", "Private infrastructure.", tier=CHECKOUT, private=True,
json_type="string", required=True),
Field("token", "string", "A secret.", tier=SECRET, json_type="string"),
Field("program", "string", "Names a program, privately.", tier=CHECKOUT, private=True,
names_command=True, json_type="string"),
Field("label", "string", "Plain checkout value.", tier=CHECKOUT, json_type="string"),
),
examples=({},),
unknown_keys=config_shapes.REJECT,
)
@pytest.fixture
def root(tmp_path: Path, monkeypatch: pytest.MonkeyPatch) -> Path:
tree = tmp_path / "instance"
tree.mkdir()
monkeypatch.setattr(config, "ROOT", tree)
monkeypatch.setattr(config_shapes, "SETTINGS_SECTIONS", (TRACKER,))
return tree
def _write(root: Path, instance: dict | None = None, local: dict | None = None) -> None:
if instance is not None:
(root / config.INSTANCE_SETTINGS_FILENAME).write_text(json.dumps(instance), encoding="utf-8")
if local is not None:
(root / config.LOCAL_SETTINGS_FILENAME).write_text(json.dumps(local), encoding="utf-8")
def _out(result) -> str:
"""The command's output with Rich's line wrapping undone."""
return " ".join(result.output.split())
def _squashed(result) -> str:
"""The output with every whitespace removed - for asserting a value is
absent, where a value Rich wrapped mid-word must still be found."""
return "".join(result.output.split())
def _read(path: Path) -> dict:
return json.loads(path.read_text(encoding="utf-8"))
def _fake_op(tmp_path: Path, monkeypatch: pytest.MonkeyPatch, *, values: dict | None = None,
locked: bool = False, hang: bool = False) -> Path:
"""An `op` that answers `op read --no-newline <ref>` from `values`, says
"not signed in" when `locked`, or never answers when `hang` - recorded in a
`.wikitool-tools.json` of the test's own, the way the preflight records it."""
lines = ["#!/bin/sh",
'[ "$1" = read ] && [ "$2" = --no-newline ] || { echo "[ERROR] bad call: $*" >&2; exit 2; }']
if hang:
lines.append("sleep 10")
if locked:
lines += ['echo "[ERROR] 2026/10/10 12:00:00 You are not currently signed in." >&2', "exit 1"]
lines.append('case "$3" in')
for ref, value in (values or {}).items():
lines.append(f" {shlex.quote(ref)}) printf '%s' {shlex.quote(value)}; exit 0 ;;")
lines += ["esac",
"""echo "[ERROR] 2026/10/10 12:00:00 could not read secret '$3': \\"item\\" isn't an item in the \\"Vault\\" vault" >&2""",
"exit 1"]
script = tmp_path / "bin" / "op"
script.parent.mkdir(exist_ok=True)
script.write_text("\n".join(lines) + "\n", encoding="utf-8")
script.chmod(0o755)
tools = tmp_path / toolpaths.FILE_NAME
tools.write_text(json.dumps({"schema": 1, "complete": True, "tools": {"op": str(script)}}), encoding="utf-8")
monkeypatch.setattr(toolpaths, "tools_file", lambda: tools)
return script
def _command_app(body) -> typer.Typer:
"""A one-command app standing in for a future consumer of the settings,
which reads them through the same two helpers."""
test_app = typer.Typer()
@test_app.command()
def run():
body()
@test_app.callback()
def _callback():
pass
return test_app
def _read_token():
typer.echo(f"length {len(_util.setting_value(_util.load_settings().section('tracker'), 'token'))}")
# --- declaring a field ----------------------------------------------------------
@pytest.mark.parametrize("flag", ["names_command", "gate"])
def test_a_field_that_names_a_program_or_a_gate_cannot_be_declared_instance(flag):
with pytest.raises(ValueError, match="never tier 'instance'"):
Field("x", "string", "x", tier=INSTANCE, **{flag: True})
assert Field("x", "string", "x", tier=CHECKOUT, **{flag: True}).user_only
def test_a_private_field_is_tier_checkout():
with pytest.raises(ValueError, match="private field is tier 'checkout'"):
Field("x", "string", "x", tier=INSTANCE, private=True)
def test_the_settings_flags_need_a_tier():
with pytest.raises(ValueError, match="need a tier"):
Field("x", "string", "x", private=True)
def test_an_unknown_tier_is_refused():
with pytest.raises(ValueError, match="tier must be one of"):
Field("x", "string", "x", tier="global")
def test_every_shipped_settings_section_is_one():
for shape in config_shapes.SETTINGS_SECTIONS:
assert settings.section_problems(shape) == []
# The four per-feature files predate the tiers and declare none.
for shape in config_shapes.SHAPES:
assert all(not spec.tier for spec in shape.fields)
# --- reading: the right file ----------------------------------------------------
def test_no_files_read_as_nothing_configured(root):
found = settings.load()
assert not found.instance_present and not found.local_present
assert not found.section("tracker").configured
assert found.section("tracker")["stale_days"] == 14
def test_values_in_their_own_files_are_read(root):
_write(root, {"schema": 1, "instance_id": "abc", "tracker": {"stale_days": 21}},
{"tracker": {"url": PRIVATE_VALUE, "label": "mine"}})
section = settings.load().section("tracker")
assert section["stale_days"] == 21
assert section["url"] == PRIVATE_VALUE
assert section["label"] == "mine"
@pytest.mark.parametrize("instance,local,file_name,key,tier", [
(None, {"tracker": {"stale_days": 7, "url": "x"}}, ".wikitool-local.json", "tracker.stale_days", "instance"),
({"tracker": {"label": "mine"}}, {"tracker": {"url": "x"}}, "wikitool.json", "tracker.label", "checkout"),
])
def test_a_value_in_the_wrong_tier_fails_naming_file_key_and_tier(root, instance, local, file_name, key, tier):
_write(root, instance, local)
with pytest.raises(ValidationError) as caught:
settings.load()
assert f"{file_name}: {key} belongs to tier '{tier}'" in str(caught.value)
result = runner.invoke(_command_app(lambda: _util.load_settings()), ["run"])
assert result.exit_code == 1
assert f"{file_name}: {key} belongs to tier '{tier}'" in _out(result)
def test_a_value_set_in_both_files_is_an_error_not_an_override(root):
_write(root, {"tracker": {"url": {"ref": "tracker_url"}}}, {"tracker": {"url": "x"}})
with pytest.raises(ValidationError, match="set in both"):
settings.load()
def test_a_program_field_is_never_in_the_committed_file_even_as_a_reference(root):
_write(root, {"tracker": {"program": {"ref": "op://V/i/f"}}}, {"tracker": {"url": "x"}})
with pytest.raises(ValidationError, match="not even as a reference"):
settings.load()
def test_a_reference_where_a_plain_value_belongs_is_refused(root):
_write(root, {"tracker": {"stale_days": {"ref": "days"}}}, {"tracker": {"url": "x"}})
with pytest.raises(ValidationError, match="takes a plain value, not a reference"):
settings.load()
def test_a_missing_required_field_is_reported_unless_a_reference_stands_in(root):
_write(root, {"tracker": {"stale_days": 3}})
with pytest.raises(ValidationError, match="tracker.url is missing"):
settings.load()
_write(root, {"tracker": {"stale_days": 3, "url": {"ref": "tracker_url"}}})
assert settings.load().section("tracker").refs["url"] == settings.Ref("tracker_url")
def test_an_unknown_section_is_reported_not_refused(root):
_write(root, {"newer": {"x": 1}})
assert settings.load().unknown == {"wikitool.json": ["newer"]}
@pytest.mark.parametrize("ref", [{"ref": "Upper"}, {"ref": "op://only/two"}, {"ref": 3},
{"ref": "x", "extra": 1}])
def test_a_malformed_reference_is_refused(root, ref):
_write(root, {"tracker": {"token": ref}}, {"tracker": {"url": "x"}})
with pytest.raises(ValidationError):
settings.load()
# --- reading: no plain secret, no plain private value in the committed file -----
@pytest.mark.parametrize("instance,local", [
({"tracker": {"token": SECRET_VALUE}}, {"tracker": {"url": "x"}}),
(None, {"tracker": {"url": "x", "token": SECRET_VALUE}}),
({"tracker": {"url": SECRET_VALUE}}, None),
])
def test_a_plain_secret_or_committed_private_value_fails_without_showing_it(root, instance, local):
_write(root, instance, local)
with pytest.raises(ValidationError) as caught:
settings.load()
assert SECRET_VALUE not in str(caught.value)
result = runner.invoke(_command_app(lambda: _util.load_settings()), ["run"])
assert result.exit_code == 1
assert SECRET_VALUE not in _squashed(result)
checks = doctor.check_settings()
assert any(c.status == "FAIL" for c in checks)
assert SECRET_VALUE not in json.dumps([c.__dict__ for c in checks])
def test_docs_verify_flags_a_plain_secret_in_the_committed_file_only(root):
_write(root, {"tracker": {"token": SECRET_VALUE, "url": SECRET_VALUE}})
problems = docs_verify.check_instance_settings()
assert len(problems) == 2
assert SECRET_VALUE not in "\n".join(problems)
# The local file is never committed, and a required field may live there.
_write(root, {"tracker": {"token": {"ref": "tracker_token"}}}, {"tracker": {"token": SECRET_VALUE}})
assert docs_verify.check_instance_settings() == []
def test_docs_verify_accepts_an_absent_committed_file(root):
assert docs_verify.check_instance_settings() == []
# --- resolving a named reference ------------------------------------------------
def test_a_named_reference_resolves_from_its_variable(root, monkeypatch):
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
{"tracker": {"url": "x"}})
monkeypatch.setenv("WIKITOOL_SECRET_TRACKER_TOKEN", SECRET_VALUE)
assert settings.load().section("tracker")["token"] == SECRET_VALUE
def test_without_the_variable_it_resolves_from_the_secrets_file_in_the_user_s_home(root, hermetic_environment):
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
{"tracker": {"url": "x"}})
secrets_file = config.secrets_file("abc")
assert secrets_file == hermetic_environment / ".config" / "chemenu" / "abc" / "secrets.json"
secrets_file.parent.mkdir(parents=True)
secrets_file.write_text(json.dumps({"tracker_token": SECRET_VALUE}), encoding="utf-8")
secrets_file.chmod(0o600)
assert settings.load().section("tracker")["token"] == SECRET_VALUE
@needs_posix
def test_a_secrets_file_others_can_read_is_refused(root, hermetic_environment):
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
{"tracker": {"url": "x"}})
secrets_file = config.secrets_file("abc")
secrets_file.parent.mkdir(parents=True)
secrets_file.write_text(json.dumps({"tracker_token": SECRET_VALUE}), encoding="utf-8")
secrets_file.chmod(0o644)
with pytest.raises(ValidationError, match="chmod 600"):
settings.load().section("tracker")["token"]
def test_an_unresolvable_name_says_where_to_put_the_value(root):
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
{"tracker": {"url": "x"}})
with pytest.raises(ValidationError, match="WIKITOOL_SECRET_TRACKER_TOKEN"):
settings.load().section("tracker")["token"]
result = runner.invoke(_command_app(_read_token), ["run"])
assert result.exit_code == 1
def test_a_reference_is_resolved_on_access_not_on_load(root):
_write(root, {"instance_id": "abc", "tracker": {"token": {"ref": "tracker_token"}}},
{"tracker": {"url": "x"}})
section = settings.load().section("tracker") # no error: nothing resolved yet
assert section["url"] == "x"
# --- resolving an op:// reference -----------------------------------------------
@needs_posix
def test_an_op_reference_resolves_through_op(root, tmp_path, monkeypatch):
_fake_op(tmp_path, monkeypatch, values={"op://Vault/tracker/url": PRIVATE_VALUE,
"op://Vault/tracker/token": SECRET_VALUE})
_write(root, {"tracker": {"url": {"ref": "op://Vault/tracker/url"},
"token": {"ref": "op://Vault/tracker/token"}}})
section = settings.load().section("tracker")
assert section["url"] == PRIVATE_VALUE
assert section["token"] == SECRET_VALUE
@needs_posix
def test_a_locked_op_is_exit_42(root, tmp_path, monkeypatch):
_fake_op(tmp_path, monkeypatch, locked=True)
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
with pytest.raises(settings.SecretSourceLocked):
settings.load().section("tracker")["token"]
result = runner.invoke(_command_app(_read_token), ["run"])
assert result.exit_code == 42
assert "NEEDS USER CLEARANCE" in _out(result)
assert "op://Vault" not in _squashed(result)
@needs_posix
def test_an_op_that_does_not_answer_in_time_is_exit_42(root, tmp_path, monkeypatch):
_fake_op(tmp_path, monkeypatch, hang=True)
monkeypatch.setattr(settings, "OP_TIMEOUT_SECONDS", 1)
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
result = runner.invoke(_command_app(_read_token), ["run"])
assert result.exit_code == 42
@needs_posix
def test_a_missing_item_is_exit_1_naming_the_reference(root, tmp_path, monkeypatch):
_fake_op(tmp_path, monkeypatch, values={})
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
result = runner.invoke(_command_app(_read_token), ["run"])
assert result.exit_code == 1
assert "op://Vault/tracker/token" in _out(result)
def test_a_missing_op_is_exit_1_pointing_at_the_preflight(root, tmp_path, monkeypatch):
tools = tmp_path / toolpaths.FILE_NAME
tools.write_text(json.dumps({"schema": 1, "complete": True, "tools": {}}), encoding="utf-8")
monkeypatch.setattr(toolpaths, "tools_file", lambda: tools)
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
result = runner.invoke(_command_app(_read_token), ["run"])
assert result.exit_code == 1
assert "preflight" in _out(result)
def test_without_a_tools_file_an_absent_op_is_exit_1_too(root, monkeypatch):
monkeypatch.setenv("PATH", "")
_write(root, {"tracker": {"token": {"ref": "op://Vault/tracker/token"}}}, {"tracker": {"url": "x"}})
result = runner.invoke(_command_app(_read_token), ["run"])
assert result.exit_code == 1
assert "1Password CLI" in _out(result)
# --- doctor ---------------------------------------------------------------------
def _by_name(checks) -> dict:
return {c.name: c for c in checks}
def test_doctor_reports_each_tier_absent_as_ok(root):
checks = _by_name(doctor.check_settings())
assert [c.status for c in checks.values()] == ["OK", "OK", "OK"]
assert "absent" in checks["settings-instance"].detail
assert "absent" in checks["settings-checkout"].detail
assert checks["settings-secrets"].detail == "no references"
def test_doctor_reports_a_malformed_file_as_fail(root):
(root / config.INSTANCE_SETTINGS_FILENAME).write_text("{not json", encoding="utf-8")
checks = _by_name(doctor.check_settings())
assert checks["settings-instance"].status == "FAIL"
assert checks["settings-checkout"].status == "OK"
@needs_posix
def test_doctor_reports_references_as_resolvable_or_not_never_their_value(root, tmp_path, monkeypatch):
_fake_op(tmp_path, monkeypatch, values={"op://Vault/tracker/url": PRIVATE_VALUE})
_write(root, {"instance_id": "abc", "tracker": {"url": {"ref": "op://Vault/tracker/url"},
"token": {"ref": "tracker_token"}}})
monkeypatch.setenv("WIKITOOL_SECRET_TRACKER_TOKEN", SECRET_VALUE)
checks = _by_name(doctor.check_settings())
assert checks["settings-secrets"].status == "OK"
assert checks["settings-secrets"].detail == "2 of 2 reference(s) resolvable"
assert "instance id abc" in checks["settings-instance"].detail
monkeypatch.delenv("WIKITOOL_SECRET_TRACKER_TOKEN")
checks = _by_name(doctor.check_settings())
assert checks["settings-secrets"].status == "FAIL"
assert "1 of 2" in checks["settings-secrets"].detail
dumped = json.dumps([c.__dict__ for c in checks.values()])
assert SECRET_VALUE not in dumped and PRIVATE_VALUE not in dumped
@needs_posix
def test_doctor_warns_rather_than_fails_on_a_locked_op(root, tmp_path, monkeypatch):
_fake_op(tmp_path, monkeypatch, locked=True)
_write(root, {"tracker": {"url": {"ref": "op://Vault/tracker/url"}}})
checks = _by_name(doctor.check_settings())
assert checks["settings-secrets"].status == "WARN"
assert "tracker.url" in checks["settings-secrets"].detail
def test_doctor_warns_on_a_section_this_version_does_not_read(root):
_write(root, {"newer": {}})
checks = _by_name(doctor.check_settings())
assert checks["settings-instance"].status == "WARN"
# --- config set -----------------------------------------------------------------
def test_config_set_writes_an_instance_value_and_creates_the_file(root):
result = runner.invoke(app, ["config", "set", "tracker.stale_days", "21"])
assert result.exit_code == 0, result.output
data = _read(root / config.INSTANCE_SETTINGS_FILENAME)
assert data["tracker"] == {"stale_days": 21}
assert data["schema"] == 1
assert data["instance_id"]
assert "created" in _out(result)
assert not (root / config.LOCAL_SETTINGS_FILENAME).exists()
instance_id = data["instance_id"]
result = runner.invoke(app, ["config", "set", "tracker.stale_days", "30"])
assert result.exit_code == 0, result.output
assert _read(root / config.INSTANCE_SETTINGS_FILENAME)["instance_id"] == instance_id
def test_config_set_refuses_a_plain_secret_without_echoing_it(root):
result = runner.invoke(app, ["config", "set", "tracker.token", SECRET_VALUE])
assert result.exit_code == 1
assert SECRET_VALUE not in _squashed(result)
assert not (root / config.INSTANCE_SETTINGS_FILENAME).exists()
def test_config_set_writes_a_secret_reference_into_the_committed_file(root):
result = runner.invoke(app, ["config", "set", "tracker.token", "--ref", "tracker_token"])
assert result.exit_code == 0, result.output
assert _read(root / config.INSTANCE_SETTINGS_FILENAME)["tracker"]["token"] == {"ref": "tracker_token"}
def test_config_set_puts_a_private_value_locally_and_a_reference_in_its_place(root):
result = runner.invoke(app, ["config", "set", "tracker.url", PRIVATE_VALUE])
assert result.exit_code == 0, result.output
assert _read(root / config.LOCAL_SETTINGS_FILENAME)["tracker"]["url"] == PRIVATE_VALUE
assert PRIVATE_VALUE not in _squashed(result)
assert "tracker" not in _read(root / config.INSTANCE_SETTINGS_FILENAME)
result = runner.invoke(app, ["config", "set", "tracker.url", "--ref", "op://Vault/tracker/url"])
assert result.exit_code == 0, result.output
assert "removed from .wikitool-local.json" in _out(result)
assert _read(root / config.INSTANCE_SETTINGS_FILENAME)["tracker"]["url"] == {"ref": "op://Vault/tracker/url"}
assert "tracker" not in _read(root / config.LOCAL_SETTINGS_FILENAME)
def test_config_set_leaves_a_program_field_to_the_user(root):
result = runner.invoke(app, ["config", "set", "tracker.program", "/usr/bin/true"])
assert result.exit_code == 42
assert ".wikitool-local.json" in _out(result)
assert not (root / config.LOCAL_SETTINGS_FILENAME).exists()
@pytest.mark.parametrize("args", [
["tracker.nope", "1"],
["nope.field", "1"],
["nodot", "1"],
["tracker.stale_days", "--ref", "days"],
["tracker.token", "--ref", "Not-A-Name"],
["tracker.stale_days", "many"],
["tracker.stale_days", "0"],
["tracker.stale_days"],
])
def test_config_set_refuses_and_writes_nothing(root, args):
result = runner.invoke(app, ["config", "set", *args])
assert result.exit_code == 1, result.output
assert not (root / config.INSTANCE_SETTINGS_FILENAME).exists()
assert not (root / config.LOCAL_SETTINGS_FILENAME).exists()
def test_config_set_refuses_on_top_of_files_that_do_not_pass(root):
_write(root, {"tracker": {"token": SECRET_VALUE}})
before = (root / config.INSTANCE_SETTINGS_FILENAME).read_text(encoding="utf-8")
result = runner.invoke(app, ["config", "set", "tracker.stale_days", "3"])
assert result.exit_code == 1
assert SECRET_VALUE not in _squashed(result)
assert (root / config.INSTANCE_SETTINGS_FILENAME).read_text(encoding="utf-8") == before
def test_config_set_with_no_section_registered_names_none(root, monkeypatch):
monkeypatch.setattr(config_shapes, "SETTINGS_SECTIONS", ())
result = runner.invoke(app, ["config", "set", "tracker.stale_days", "3"])
assert result.exit_code == 1
assert "none is registered" in _out(result)
# --- the two files in git -------------------------------------------------------
def test_the_repository_ignores_the_local_file_and_tracks_the_committed_one():
root = config._PACKAGE_ROOT
probe = subprocess.run(
["git", "-C", str(root), "check-ignore", "--no-index", "-q", config.LOCAL_SETTINGS_FILENAME],
capture_output=True, check=False,
)
if probe.returncode not in (0, 1):
pytest.skip("no git checkout to ask")
assert probe.returncode == 0
tracked = subprocess.run(
["git", "-C", str(root), "check-ignore", "--no-index", "-q", config.INSTANCE_SETTINGS_FILENAME],
capture_output=True, check=False,
)
assert tracked.returncode == 1
# --- CI: a real op:// value -------------------------------------------------------
def test_live_secret_reference_resolves_from_ci(root, monkeypatch):
"""CI loads `op://CI-CD/...` through 1Password's own action into
`WIKITOOL_SECRET_CI_SMOKE` (.gitea/workflows/ci.yml) and runs this test with
`CHEMENU_LIVE_SECRET_REQUIRE=1`, which turns "no value" into a failure. The
value is never printed - only that a reference resolved to something."""
if not _LIVE_VALUE:
if _LIVE_REQUIRED:
pytest.fail("CHEMENU_LIVE_SECRET_REQUIRE=1 but WIKITOOL_SECRET_CI_SMOKE is empty - "
"the 1Password step did not load it")
pytest.skip("no WIKITOOL_SECRET_CI_SMOKE - set only in CI, by the 1Password step")
monkeypatch.setenv("WIKITOOL_SECRET_CI_SMOKE", _LIVE_VALUE)
_write(root, {"instance_id": "ci", "tracker": {"token": {"ref": "ci_smoke"}}}, {"tracker": {"url": "x"}})
assert settings.load().section("tracker")["token"] == _LIVE_VALUE
checks = _by_name(doctor.check_settings())
assert checks["settings-secrets"].detail == "1 of 1 reference(s) resolvable"