Files
FabledScribe/tests/test_version_readout.py
bvandeusenandClaude Opus 5 9bb59b73ba
CI & Build / Python lint (push) Successful in 3s
CI & Build / Plugin hooks (push) Successful in 7s
CI & Build / TypeScript typecheck (push) Successful in 10s
CI & Build / integration (push) Successful in 31s
CI & Build / Python tests (push) Successful in 1m5s
CI & Build / Build & push image (push) Successful in 38s
feat(frontend): the app says what it is running, and says so honestly when it cannot find out (#3329)
#3127 checklist 12, plus rule 27 — a capability with no surface the operator
can touch is not shipped.

The step was planned on the premise that nothing read `/api/version`. Two
things did, and the state was worse than nothing:

- `App.vue` fetched it, wrote `version` into a ref initialised to the literal
  `"dev"`, and swallowed the error. An instance that could not answer rendered
  EXACTLY what a healthy local build renders. That is checklist 12's named
  failure — a blank standing in for `unknown` — in the one readout whose whole
  job is to say what is running, and it would have made #3298's debugging
  session no cheaper.
- `SettingsView.vue` fetched the same endpoint again on every mount and wrote
  the result into a local ref no template ever read. A duplicate request whose
  answer was discarded.

So this is not "add a readout"; it is "make the existing one honest, and give
it the three fields nobody could see."

The readout — Settings → Config, first section, beside the other "what is this
instance doing" facts. Three states kept apart, because collapsing any two of
them is the defect:

  not asked yet (tab unopened)   nothing
  answered                       the values, each ABSENT field as "unknown"
  the fetch itself failed        its own message, with a retry

`version` and `channel` prominent, `commit` in full with a copy button so it
can be pasted into a `:sha` lookup (rule 145 — the registry's identity and the
artifact's own must be checkable against each other), `build` kept because its
ABSENCE is the diagnostic part: no ordering key means this build is not in any
update order, which is what a local or hand-built image looks like.

Absence, not falsiness. The payload omits what it does not know rather than
sending `""` or `0` (see `build_version_payload`), so the renderer uses `??`
throughout — `build` is a number and `0` is a legitimate ordering key, which
`||` would report as unknown. `tests/test_version_readout.py` pins that
operator specifically, along with the "no plausible default" property, because
`||` is the form a person reaches for by habit.

Rule 156 — the fetch carries a deadline. This readout is consulted when an
instance is misbehaving, which is exactly when it may never answer; without one
the surface sits on "still loading" forever, which is the same blank arrived at
from the other direction. `apiGet` gains an OPT-IN `timeoutMs` rather than a
default, so no existing call site's behaviour moves. Every other call in the
client still has no deadline — reported separately, not fixed here.

No frontend test runner exists, so verification is the typecheck lane plus four
source-inspection guards in the unit lane, each pinning one property.

Also folded in: `plugin/README.md` now leads with the mint script and offers
`make` second, since `make` is not installed on every workstation.

Co-Authored-By: Claude Opus 5 (1M context) <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01TcCs1CcQ1ormdnzSshKqvN
2026-09-02 11:23:43 -04:00

87 lines
4.0 KiB
Python

"""The app must SAY what it is running, and must not lie when it cannot find out.
There is no frontend test runner in this repo, so these are source-inspection
guards in the unit lane — the same idiom `check_plugin.py` uses on the hook
shells. They are deliberately few and deliberately about ONE property each,
because a grep-shaped test that asserts a whole file's contents fails on every
refactor and gets deleted.
WHY THIS FILE EXISTS. #3298: with a deploy misbehaving, nothing on the instance
could say which commit was serving it, and the one endpoint whose job that is
answered with the name of a branch. The value was fixed then. This is the other
half — the value reaching a person — and #3127 checklist 12 is specific about
the way it goes wrong: *never let a blank stand in for `unknown`*. A readout
that renders a plausible value it never received is worse than one that renders
nothing, because it ends the investigation instead of starting it.
"""
from __future__ import annotations
import re
from pathlib import Path
FRONTEND = Path(__file__).resolve().parents[1] / "frontend" / "src"
def test_something_actually_reads_the_version_endpoint():
"""The endpoint is not enough; something must ask it.
`/api/version` answered correctly for weeks with no caller — an endpoint
reachable only by someone who already knew to curl it. Rule 27: a
capability with no surface the operator can touch is not shipped.
"""
hits = [p for p in FRONTEND.rglob("*.ts") if "/api/version" in p.read_text()]
assert hits, "nothing under frontend/src fetches /api/version"
def test_the_footer_does_not_default_to_a_plausible_version():
"""The regression this readout was built to remove.
`appVersion` used to start life as the literal `"dev"` and the fetch
swallowed its own failure, so an instance that could not answer rendered
exactly what a healthy local build renders. Two very different states, one
string, and no way to tell them apart from the page.
Pinned as "the ref does not start at a version-shaped literal" rather than
as an exact initialiser, so a later refactor can change how the state is
held without failing here — what must not come back is the plausible
default.
"""
app = (FRONTEND / "App.vue").read_text()
match = re.search(r"const appVersion = ref[^;]*;", app)
assert match, "App.vue no longer declares appVersion — update this guard"
decl = match.group(0)
assert '"dev"' not in decl and "'dev'" not in decl, (
f"appVersion defaults to a version-shaped literal: {decl}\n"
"A failed fetch would render as a real-looking version (#3127 "
"checklist 12). Start from a not-answered-yet value instead."
)
def test_optional_version_fields_are_read_by_absence_not_falsiness():
"""`build` is a number and 0 is a legitimate ordering key.
The payload omits what it does not know rather than sending `""` or `0`, so
the renderer's job is to distinguish ABSENT from present. `||` cannot: it
would report a real `build` of 0 as unknown, and it is the form a person
reaches for by habit. `??` is the correct one, which is why this pins the
operator rather than the rendered output.
"""
view = (FRONTEND / "views" / "SettingsView.vue").read_text()
for field in ("channel", "build"):
assert f'versionInfo.{field} ?? "unknown"' in view, (
f"the {field} readout must use `?? \"unknown\"`, never `|| \"unknown\"` — "
"an absent field and a falsy one are different answers"
)
def test_the_version_request_carries_a_deadline():
"""Rule 156. A wait with no deadline cannot report that it failed.
This readout is consulted when an instance is misbehaving, which is exactly
when it may never answer. Without a deadline the surface sits on "still
loading" forever — the blank standing in for `unknown` again, arrived at
from the other direction.
"""
src = (FRONTEND / "api" / "version.ts").read_text()
assert "timeoutMs" in src, "the version fetch must pass a deadline"