test: tie the install docs to the code they quote (3422 follow-up)
CI / lint (push) Successful in 4s
CI / extension-version (push) Successful in 4s
Build images / sign-extension (push) Successful in 4s
Build images / build-ml (push) Successful in 7s
Build images / build-agent (push) Successful in 7s
Build images / build-web (push) Successful in 5s
Build images / smoke-web (push) Skipped
Build images / promote (push) Skipped
CI / frontend-build (push) Successful in 24s
CI / backend-lint-and-test (push) Successful in 38s
CI / integration (push) Successful in 2m11s

3422 was already fixed. Commit 86abaf0 applied option 1 in full and it is on main: .env.example carries the bootstrap section with the backup warning, README explains the refusal and why it is deliberate, docker-compose forwards the variable, and the milestone-362 smoke gate that FOUND the bug now sets it (build.yml:1159) and passes. The issue's premise - "CURATOR_BOOTSTRAP_NEW_KEY appears nowhere outside backend/" - is stale.

What was left is the dependency that fix created. README.md and .env.example now both print the literal error text, the literal key path and the variable name, because a stranger greps for the string their terminal showed them. That is the right call and it means two user-facing files now depend on this module's wording with nothing connecting them - the install surface's characteristic defect, one rename away from a README that sends strangers to a path that does not exist.

Three guards, all presence checks on both sides. An absence check against prose would pass for the wrong reason the moment a sentence were reworded (snippet 3352):

  - the raised message still contains the sentence README reproduces, the variable both docs say to set, and the restore-rather-than-mint alternative the whole refusal rests on;
  - both docs still name _CREDENTIAL_KEY_PATH and the variable, read from the code rather than retyped, so a rename fails here;
  - compose still forwards the variable - without that line the docs' "set it in .env" is silently inert and fails identically to not setting it.

Option 2 (mint when the credential table is empty) is deliberately NOT done. The issue's own guidance is "(1) now, (2) if the friction proves annoying", and the friction has not been reported. Worth recording that its predicate checks out exactly: the Fernet key protects Credential.encrypted_blob and nothing else - no other Fernet user exists - so "no credential rows means nothing can be made undecryptable" is provable rather than probable. The cost is placement: create_app() is sync and constructs the key before any engine exists, so the check cannot live where the failure is. entrypoint.sh, which already runs alembic against the DB, is the natural seam. Only the web role is affected; the Celery roles build the key lazily inside tasks.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01SHQB1YukL3VyvMK8rcbmV9
This commit is contained in:
2026-09-12 21:31:21 -04:00
co-authored by Claude Opus 5
parent eb6e0df858
commit 529d4bff57
+66
View File
@@ -69,3 +69,69 @@ def test_missing_key_with_env_var_bootstraps(tmp_path, monkeypatch):
key_path = tmp_path / "bootstrap.b64"
CredentialCrypto(key_path) # no bootstrap_ok kwarg — relies on env
assert key_path.exists()
# --- #3422: the install docs quote this module, so they must keep agreeing --
#
# README.md and .env.example both print the literal failure a new installer
# hits, and the literal path and env var they must act on. That is the right
# call — a stranger greps for the string their terminal showed them — but it
# means those two files now DEPEND on this module's wording, with nothing
# connecting them. The characteristic defect of the install surface is exactly
# this: the documented behaviour and the code drift apart, and the code is the
# one that is right.
#
# Presence checks on both sides, deliberately: an absence check against prose
# would pass for the wrong reason the moment a sentence were reworded
# (snippet #3352).
_REPO_ROOT = Path(__file__).resolve().parents[1]
_INSTALL_DOCS = ("README.md", ".env.example")
def test_the_bootstrap_refusal_still_reads_the_way_the_docs_quote_it(tmp_path, monkeypatch):
"""The three things a reader is told to look for, in the raised message."""
from backend.app.services.credential_crypto import (
_BOOTSTRAP_ENV_VAR,
MissingCredentialKey,
)
monkeypatch.delenv(_BOOTSTRAP_ENV_VAR, raising=False)
with pytest.raises(MissingCredentialKey) as exc:
CredentialCrypto(tmp_path / "absent.b64")
message = str(exc.value)
# The sentence README.md reproduces verbatim.
assert "Fernet key file not found at" in message
# The variable both docs tell the operator to set.
assert _BOOTSTRAP_ENV_VAR in message
# The alternative the docs lean on — that a restored instance restores the
# key rather than minting one. Losing this line loses the whole point.
assert "restore the key file" in message
def test_the_install_docs_name_the_real_key_path_and_variable():
"""Pins the two literals, read from the code rather than retyped here.
Changing `_CREDENTIAL_KEY_PATH` or the env var name without updating the
docs fails this — which is the only thing standing between a rename and a
README that sends strangers to a path that does not exist.
"""
from backend.app import _CREDENTIAL_KEY_PATH
from backend.app.services.credential_crypto import _BOOTSTRAP_ENV_VAR
for name in _INSTALL_DOCS:
text = (_REPO_ROOT / name).read_text()
assert str(_CREDENTIAL_KEY_PATH) in text, f"{name} does not name the key path"
assert _BOOTSTRAP_ENV_VAR in text, f"{name} does not name the bootstrap variable"
def test_compose_passes_the_bootstrap_variable_through():
"""The docs say "set it in .env"; that only works if compose forwards it.
Without this line the instruction is silently inert — the operator sets the
variable, the container never sees it, and the failure is identical to not
having set it at all.
"""
from backend.app.services.credential_crypto import _BOOTSTRAP_ENV_VAR
compose = (_REPO_ROOT / "docker-compose.yml").read_text()
assert f"{_BOOTSTRAP_ENV_VAR}: ${{{_BOOTSTRAP_ENV_VAR}" in compose