Files
FabledGrimoire/tests/test_version_endpoint.py
T
bvandeusenandClaude Opus 5.5 fa41e006a7 docs(6209): lesson links, design roles, version readout and endpoint, timestamp filters, settings defaults, opened ledger, surfaces
Eight more test files state their properties without issue, milestone and
rule numbers or dated regressions. The version tests describe what folding
the channel into the version breaks instead of the date it did; the surface
registry and settings-defaults tests state that floors move rather than
quoting the decision that made them move.

Co-Authored-By: Claude Opus 5.5 <noreply@anthropic.com>
2026-10-21 21:32:13 -04:00

181 lines
7.5 KiB
Python

"""`/api/version` reports three values, and never folds them together.
WHAT THIS IS ABOUT. A CI that sets `BUILD_VERSION` to the CHANNEL makes a
running instance answer "which build are you?" with the name of a branch — and
when a deploy behaves as though it holds older code, the one endpoint whose job
is to settle that cannot.
The three values answer different questions and so cannot be one value:
version the NAME, from COMMIT time — "is this the same code?"
build the ORDERING KEY, BUILD time — "may this be installed over that?"
channel its own field — "which line is this?"
These pin the SHAPE the lanes emit, not the values — a test asserting today's
timestamp would fail tomorrow, and one asserting the format catches the thing
that actually breaks: a channel creeping back into the name, or an ordering
key that is not orderable.
"""
import os
import pathlib
import re
from datetime import datetime, timezone
from unittest.mock import patch
import pytest
CI = pathlib.Path(__file__).resolve().parents[1] / ".forgejo/workflows/ci.yml"
# The NAME's shape: four dot-separated numeric fields, zero-padded, and
# nothing else. A channel token anywhere in here is the bug this file exists
# to prevent.
NAME_RE = re.compile(r"^\d{4}\.\d{2}\.\d{2}\.\d{4}$")
_ENV_KEYS = ("APP_VERSION", "APP_BUILD_KEY", "APP_CHANNEL", "APP_COMMIT")
def _version_payload(env: dict) -> dict:
"""The real payload builder, under a controlled environment.
Calls `build_version_payload` rather than the route: the payload is the
behaviour, and reaching it through an app and a request context would
make these tests depend on app startup to assert a dict. The route is a
one-line `jsonify` wrapper over this.
"""
from scribe.routes.api import build_version_payload
with patch.dict(os.environ, env, clear=False):
# patch.dict cannot REMOVE, and "absent" is exactly what several of
# these assert — so anything the caller left out is cleared.
for key in _ENV_KEYS:
if key not in env:
os.environ.pop(key, None)
return build_version_payload()
def test_the_three_values_are_three_fields():
"""The headline. One field cannot answer three questions, and the failure
mode of trying is silent: the string looks plausible and orders wrong."""
out = _version_payload({
"APP_VERSION": "2026.08.31.0403",
"APP_BUILD_KEY": "3505443",
"APP_CHANNEL": "stable",
"APP_COMMIT": "b267037",
})
assert out["version"] == "2026.08.31.0403"
assert out["build"] == 3505443
assert out["channel"] == "stable"
assert out["commit"] == "b267037"
def test_the_channel_is_never_inside_the_name():
"""The regression itself. `{"version": "main"}` is what this catches."""
out = _version_payload({
"APP_VERSION": "2026.08.31.0403", "APP_CHANNEL": "stable",
})
assert NAME_RE.match(out["version"]), (
f"the version name is {out['version']!r} — not YYYY.MM.DD.HHMM. A "
f"channel or branch name here is the channel folded into the version."
)
assert "stable" not in out["version"]
def test_the_ordering_key_is_an_INTEGER():
"""A string ordering key is how a comparison silently becomes
lexicographic — "9" > "10" — which reads fine and orders wrong."""
out = _version_payload({"APP_VERSION": "x", "APP_BUILD_KEY": "3505443"})
assert isinstance(out["build"], int)
assert not isinstance(out["build"], bool)
def test_unknown_values_are_ABSENT_not_empty():
"""A local build genuinely has no ordering key and no channel. Emitting
`""` or a placeholder would let it claim a position in an update order it
is not part of; a reader must see "cannot be ordered", not zero."""
out = _version_payload({"APP_VERSION": "dev"})
assert out == {"version": "dev"}
assert "build" not in out and "channel" not in out and "commit" not in out
def test_an_empty_env_var_counts_as_absent():
"""Docker sets an ARG with no default to the empty string, so "unset" and
"set to nothing" both reach the handler as ''."""
out = _version_payload({
"APP_VERSION": "dev", "APP_CHANNEL": "", "APP_BUILD_KEY": "",
"APP_COMMIT": " ",
})
assert out == {"version": "dev"}
def test_a_malformed_ordering_key_is_dropped_not_passed_through():
"""A reader that cannot order is correct; one that orders on garbage is
not. Dropping it degrades to "unorderable", which is a state the caller
already has to handle."""
out = _version_payload({"APP_VERSION": "dev", "APP_BUILD_KEY": "main"})
assert "build" not in out
def test_the_channel_is_reported_verbatim():
"""Never validated against an enum — a build claiming something
unexpected is better shown than dropped."""
out = _version_payload({"APP_VERSION": "dev", "APP_CHANNEL": "canary"})
assert out["channel"] == "canary"
# ── The lane, as CI actually writes it ─────────────────────────────────
def test_ci_does_not_stamp_the_channel_as_the_version():
"""The bug lived in the workflow, not the handler. A correct handler fed
`BUILD_VERSION=main` still reports a branch name."""
text = CI.read_text()
assert "BUILD_VERSION=${{ steps.tags.outputs.build_name }}" in text, (
"CI no longer passes the derived NAME as BUILD_VERSION. If it is "
"passing a branch or channel again, /api/version is lying."
)
for wrong in ('BUILD_VERSION="main"', 'BUILD_VERSION="dev"'):
assert wrong not in text, (
f"CI sets {wrong} — that is the channel in the version field, "
f"folding the three values into one."
)
def test_ci_derives_the_name_from_COMMIT_time_and_the_key_from_BUILD_time():
"""The two clocks are deliberate and easy to "tidy" into one.
The name must come from the commit so two lanes building one source agree;
the key must come from the build so it cannot go backwards when an older
commit is rebuilt. Collapsing them breaks whichever question loses.
"""
text = CI.read_text()
assert "git log --format=%ct -1 HEAD" in text, (
"the version NAME is no longer derived from commit time — two lanes "
"building the same commit will now report different strings"
)
assert "$(date -u +%s) - 1577836800" in text, (
"the ORDERING KEY is no longer minutes-since-2020 from build time; "
"if it now comes from the commit it can go backwards on a rebuild"
)
def test_ci_passes_all_three_plus_the_commit():
text = CI.read_text()
for arg in ("BUILD_KEY=", "BUILD_CHANNEL=", "BUILD_COMMIT="):
assert arg in text, f"CI no longer passes {arg} to the image build"
@pytest.mark.parametrize("commit_epoch,expected", [
# Midnight, where a naive formatter drops the leading zeros and yields
# "2026.01.05.0" — the case the padding exists for.
(datetime(2026, 1, 5, 0, 0, tzinfo=timezone.utc), "2026.01.05.0000"),
(datetime(2026, 1, 5, 0, 7, tzinfo=timezone.utc), "2026.01.05.0007"),
(datetime(2026, 12, 31, 23, 59, tzinfo=timezone.utc), "2026.12.31.2359"),
(datetime(2026, 8, 31, 4, 3, tzinfo=timezone.utc), "2026.08.31.0403"),
])
def test_the_name_format_zero_pads_every_field(commit_epoch, expected):
"""`date -u +%Y.%m.%d.%H%M` is what CI runs; this pins what that must
produce, so a reformat that loses zero-padding fails here rather than in
a comparison months later."""
assert commit_epoch.strftime("%Y.%m.%d.%H%M") == expected
assert NAME_RE.match(expected)