feat: the agent derives its version instead of being asked to remember it (4386)
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 2s
CI and images / frontend-build (push) Successful in 19s
extension / lint (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 33s
CI and images / integration (push) Successful in 2m25s
CI and images / sign-extension (push) Successful in 4s
CI and images / build-web (push) Successful in 2m1s
CI and images / smoke-web (push) Successful in 1m2s
CI and images / build-agent (push) Successful in 10m7s
CI and images / promote (push) Skipped
CI and images / lint (push) Successful in 3s
CI and images / extension-version (push) Successful in 2s
CI and images / frontend-build (push) Successful in 19s
extension / lint (push) Successful in 21s
CI and images / backend-lint-and-test (push) Successful in 33s
CI and images / integration (push) Successful in 2m25s
CI and images / sign-extension (push) Successful in 4s
CI and images / build-web (push) Successful in 2m1s
CI and images / smoke-web (push) Successful in 1m2s
CI and images / build-agent (push) Successful in 10m7s
CI and images / promote (push) Skipped
Operator: the agent's build string could not identify the agent. VERSION was a
literal in app.py an author was meant to bump, and nobody did — the September
image printed the same "2026-07-17.1" as the July one, so the one surface
meant to answer "did my pull work?" answered the same either way.
Nothing new was needed. scripts/artifacts.sh has derived a version per
artifact since milestone 313, and build-agent has been computing the agent's
on every run and printing it to the log. The image just never carried it.
Three values, never folded together (rule 149):
FC_VERSION YYYY.MM.DD.HHMM from the COMMIT its shipped files last changed
in — identical on dev and main for the same source, which is
what makes "am I running production's code?" answerable.
FC_CHANNEL a sibling field, never a suffix inside the name.
FC_REVISION the 12-char sha; the same string as the fc.revision LABEL, so
the image and the registry cannot disagree about which commit
this is.
The page SHOWS the version and COMPARES the revision. Those were one value
before, which is how a version acquires a second job and then cannot be
changed without breaking the reload banner. An unstamped local build reads
`unknown` and compares `local` — absent rather than empty, one spelling of
"cannot say".
scripts/artifacts.sh joins the AGENT path set in the same commit, and it had
to: a version has no backstop. A revision that is computed differently stops
matching the published label and forces a rebuild, so it self-corrects; a
version is compared against nothing, so a change to cmd_version alone would
leave the agent publishing the old format with nothing to contradict it. That
is #3202's finding, and the agent was rightly exempt only while it had no
version of its own. tests/test_artifact_paths.py pins it.
Also corrects two build.yml comments claiming agent/ had not changed since
2026-07-17. Both were already false — it changed 2026-09-23 — and one of them
is the stated rationale for the force_build escape hatch. Rewritten without
dates: how long an artifact has been quiet is a `git log` question, and its
answer in a comment is wrong the next time anyone commits (lesson #4383).
Co-Authored-By: Claude Opus 5 <noreply@anthropic.com>
Claude-Session: https://claude.ai/code/session_01LVjrnpQjRgHdvq95rASoiR
This commit is contained in:
@@ -41,10 +41,17 @@ on:
|
||||
# produce a changelog, not an image.
|
||||
|
||||
# The escape hatch for the one thing skip-if-exists makes untestable: a
|
||||
# build that WOULD be skipped. `agent/` has not changed since 2026-07-17, so
|
||||
# every push since has correctly declined to build it — which also means the
|
||||
# agent build path has not run in six weeks and cannot be exercised on
|
||||
# demand. #3190 lives on exactly that path.
|
||||
# build that WOULD be skipped. An artifact whose source is quiet has its
|
||||
# build path correctly declined on every push — which also means that path
|
||||
# is not exercised, and cannot be exercised on demand, for as long as the
|
||||
# quiet lasts. #3190 lives on exactly such a path.
|
||||
#
|
||||
# This used to name the agent and a date ("unchanged since 2026-07-17"). It
|
||||
# was already false when read on 2026-09-24 — the agent had changed the day
|
||||
# before — and a rationale resting on a stale fact reads as settled
|
||||
# reasoning forever after (lesson #4383). Which artifact is quiet is a
|
||||
# question for `git log`, not for a comment; the hatch exists because ANY of
|
||||
# them can be.
|
||||
#
|
||||
# Editing build.yml does not force one either, and that is deliberate: the
|
||||
# workflow is not shipped bytes, so it is in no artifact's path set. Putting
|
||||
@@ -67,11 +74,18 @@ on:
|
||||
# The base-image refresh (milestone 326 step 4, #3154).
|
||||
#
|
||||
# Skip-if-exists is keyed on OUR source, so an artifact whose source stops
|
||||
# moving stops picking up base-image updates. `agent/` last changed
|
||||
# 2026-07-17; every push since has correctly declined to rebuild it, which
|
||||
# also means it will serve that day's `nvidia/cuda` layers forever. Nothing
|
||||
# is wrong until it has been unchanged for months, which is precisely why
|
||||
# this is a calendar trigger and not a condition on the push path.
|
||||
# moving stops picking up base-image updates — it will serve the base layers
|
||||
# of its last build forever. The agent is the standing example because its
|
||||
# base is the heaviest (`nvidia/cuda`, ~6.3 GB) and its source the quietest:
|
||||
# 48 of 1111 commits touch it, about 4% (#3114).
|
||||
#
|
||||
# Deliberately no date here. Nothing is wrong until an artifact has been
|
||||
# unchanged for months, and "how long has it been" is a `git log` question
|
||||
# whose answer in a comment is wrong the next time anyone commits — which is
|
||||
# exactly how the previous version of this paragraph came to claim the agent
|
||||
# had not moved since 2026-07-17 while it had moved the day before. That is
|
||||
# precisely why this is a CALENDAR trigger and not a condition on the push
|
||||
# path: the calendar cannot be wrong about how much time has passed.
|
||||
#
|
||||
# Weekly, Sunday 06:00 UTC. Away from CI-runner's Monday security sweep so
|
||||
# the two are never diagnosing each other, and on the quietest day so a
|
||||
@@ -1352,9 +1366,11 @@ jobs:
|
||||
# :dev than to the :2026.8.28 tags milestone 318 deleted. (#3114.)
|
||||
cache-from: type=registry,ref=git.fabledsword.com/bvandeusen/fabledcurator:buildcache
|
||||
cache-to: type=registry,ref=git.fabledsword.com/bvandeusen/fabledcurator:buildcache,mode=max
|
||||
# Only the web image carries these: it is the one with a UI and an
|
||||
# HTTP surface to report them on. The ml and agent images have
|
||||
# nothing to tell.
|
||||
# The agent carries its own copy of these (plus FC_REVISION) since
|
||||
# 2026-09-24. This used to say the agent "has nothing to tell" — true
|
||||
# of the ml image, which no longer exists (#4311), and never true of
|
||||
# the agent, which has a control page and a /status endpoint and was
|
||||
# reporting a hand-written literal on both.
|
||||
build-args: |
|
||||
FC_CHANNEL=${{ steps.tag.outputs.channel }}
|
||||
FC_VERSION=${{ steps.reuse.outputs.version }}
|
||||
@@ -2200,6 +2216,13 @@ jobs:
|
||||
set -eu
|
||||
DERIVED=$(sh scripts/artifacts.sh revision agent)
|
||||
echo "revision=$DERIVED" >> "$GITHUB_OUTPUT"
|
||||
# Baked into the agent image as FC_VERSION and reported by its
|
||||
# control page and /status. A pure function of the revision — same
|
||||
# commit, same string — so it adds no variability the reuse check
|
||||
# would have to account for. Added 2026-09-24: the agent's version
|
||||
# used to be a hand-written literal in app.py that nobody bumped, so
|
||||
# the September image reported the same string as the July one.
|
||||
echo "version=$(sh scripts/artifacts.sh version agent)" >> "$GITHUB_OUTPUT"
|
||||
|
||||
# The build clock, pinned to the same commit (#3265). Without it
|
||||
# buildkit stamps the image config with the wall clock of the build,
|
||||
@@ -2353,6 +2376,16 @@ jobs:
|
||||
# decoration — an unstamped image is one that will always rebuild.
|
||||
labels: |
|
||||
fc.revision=${{ steps.reuse.outputs.revision }}
|
||||
# What the agent reports about ITSELF, as opposed to the label above,
|
||||
# which is what the registry reports about it. Three values kept
|
||||
# apart (rule 149). FC_REVISION is the same string as the label, so
|
||||
# the image and the registry cannot disagree about which commit this
|
||||
# is — a build whose self-report names a different commit from the
|
||||
# tag it was published under is unrollbackable in practice.
|
||||
build-args: |
|
||||
FC_CHANNEL=${{ steps.tag.outputs.channel }}
|
||||
FC_VERSION=${{ steps.reuse.outputs.version }}
|
||||
FC_REVISION=${{ steps.reuse.outputs.revision }}
|
||||
# LOAD-BEARING, not a preference. On the default docker driver these
|
||||
# were no-ops; on the docker-container driver above,
|
||||
# build-push-action@v5 defaults provenance to TRUE when pushing.
|
||||
|
||||
@@ -27,6 +27,23 @@ COPY fc_agent ./fc_agent
|
||||
# imgutils ONNX models + the transformers SigLIP weights both cache here; mount
|
||||
# a volume to persist them across restarts (the SigLIP download is ~3.5 GB once).
|
||||
ENV HF_HOME=/models
|
||||
|
||||
# Declared LAST on purpose, exactly as the web Dockerfile does: an ARG/ENV
|
||||
# invalidates every layer below it, and these are the only values that differ
|
||||
# between builds of otherwise identical source. Any earlier and the ~6.3 GB
|
||||
# CUDA + torch layers could never be shared between the dev and main builds of
|
||||
# one commit — which is the cost #3114 measured at 9m26s cold.
|
||||
#
|
||||
# Three values, never folded together (rule 149) — the NAME a person reads, the
|
||||
# CHANNEL it came from, and the REVISION that identifies the content. See
|
||||
# fc_agent/build_info.py; CI derives all three from scripts/artifacts.sh.
|
||||
ARG FC_CHANNEL=""
|
||||
ENV FC_CHANNEL=${FC_CHANNEL}
|
||||
ARG FC_VERSION=""
|
||||
ENV FC_VERSION=${FC_VERSION}
|
||||
ARG FC_REVISION=""
|
||||
ENV FC_REVISION=${FC_REVISION}
|
||||
|
||||
EXPOSE 8770
|
||||
|
||||
# The control UI; the worker is started from it (or POST /start).
|
||||
|
||||
+27
-8
@@ -12,16 +12,21 @@ from fastapi import FastAPI, Request
|
||||
from fastapi.responses import HTMLResponse, JSONResponse
|
||||
|
||||
from . import logbuf
|
||||
from .build_info import FC_CHANNEL, FC_REVISION, FC_VERSION, build_id, display_version
|
||||
from .config import Config
|
||||
from .gpu import read_gpu
|
||||
from .worker import Worker
|
||||
|
||||
log = logging.getLogger("fc_agent.app")
|
||||
|
||||
# Bump on every agent change. The page embeds this and /status reports it; the UI
|
||||
# warns to reload when they differ — so a stale browser-cached page can't be
|
||||
# mistaken for "the new image didn't deploy". (Belt-and-braces with no-store.)
|
||||
VERSION = "2026-07-17.1 · idle model-unload: after ~5 min idle the GPU models release their VRAM and reload on the next job (env IDLE_UNLOAD_SECONDS, 0=off) · sleep mode sheds to one downloader"
|
||||
# DERIVED at image build time, not hand-maintained — see build_info. This was a
|
||||
# literal an author was asked to bump on every agent change, and the September
|
||||
# image printed the same "2026-07-17.1" as the July one, so the surface meant to
|
||||
# answer "did my pull work?" answered the same either way.
|
||||
#
|
||||
# Two values with two jobs, kept apart (rule 149): the page SHOWS the version
|
||||
# and COMPARES the build id. /status reports both, plus the raw fields, so a
|
||||
# reader never has to take a formatted string apart to get at one of them.
|
||||
|
||||
logbuf.install()
|
||||
cfg = Config.from_env()
|
||||
@@ -52,7 +57,14 @@ def _maybe_autostart() -> None:
|
||||
|
||||
@app.get("/", response_class=HTMLResponse)
|
||||
def index() -> str:
|
||||
return _PAGE.replace("__BUILD__", VERSION)
|
||||
# Two substitutions, not one: `__VERSION__` is what a person reads in the
|
||||
# meta line, `__BUILD_ID__` is what the script compares against /status to
|
||||
# notice the page is a cached copy from a previous build.
|
||||
return (
|
||||
_PAGE
|
||||
.replace("__VERSION__", display_version())
|
||||
.replace("__BUILD_ID__", build_id())
|
||||
)
|
||||
|
||||
|
||||
@app.post("/start")
|
||||
@@ -117,7 +129,14 @@ def status():
|
||||
s["fc_url"] = cfg.fc_url
|
||||
s["configured"] = bool(cfg.token)
|
||||
s["queue"] = worker.latest_queue()
|
||||
s["build"] = VERSION
|
||||
# `build` is the comparison token the page checks — see build_info.
|
||||
# `version`/`channel`/`revision` ride BESIDE it rather than inside it, so a
|
||||
# reader wanting the version never has to parse it back out of something
|
||||
# else. Absent rather than empty when the image carries no stamp.
|
||||
s["build"] = build_id()
|
||||
s["version"] = FC_VERSION or None
|
||||
s["channel"] = FC_CHANNEL or None
|
||||
s["revision"] = FC_REVISION or None
|
||||
return JSONResponse(s)
|
||||
|
||||
|
||||
@@ -203,7 +222,7 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
||||
<div class=brand><span class=logo>◆</span> FabledCurator <span class=sub>GPU agent</span></div>
|
||||
<div class=conn><span class="dot" id=dot></span><span id=connlbl>—</span></div>
|
||||
</header>
|
||||
<p class=meta>Server <code id=fc>—</code> · token <code id=cfg>—</code> · build <code id=build>__BUILD__</code></p>
|
||||
<p class=meta>Server <code id=fc>—</code> · token <code id=cfg>—</code> · build <code id=build>__VERSION__</code></p>
|
||||
|
||||
<div id=verbanner class=banner style="display:none;background:#3a1212;border-color:#5a1717;color:#ffb3b3">
|
||||
a newer agent version is running — reload this page (Ctrl+Shift+R) to update the controls
|
||||
@@ -262,7 +281,7 @@ _PAGE = """<!doctype html><html><head><meta charset=utf-8>
|
||||
</section>
|
||||
</div>
|
||||
<script>
|
||||
const PAGE_BUILD="__BUILD__"
|
||||
const PAGE_BUILD="__BUILD_ID__"
|
||||
let CAP=8
|
||||
// Optimistic transitional state on click, then apply the POST's own status
|
||||
// response (it returns worker.status()) for instant feedback — don't wait on the
|
||||
|
||||
@@ -0,0 +1,79 @@
|
||||
"""What this agent build IS — stamped at image build time, not configurable.
|
||||
|
||||
The mirror of `backend/app/build_info.py`, for the same reasons and with the
|
||||
same posture. Kept as its own module rather than as constants in `app.py`
|
||||
because it is stdlib-only and therefore importable by the test suite, which
|
||||
cannot import `app` (torch, transformers and ultralytics are not in the CI
|
||||
image — see build.yml's "Agent syntax check").
|
||||
|
||||
## Why this replaced a hand-written string
|
||||
|
||||
`app.VERSION` used to be a literal an author was asked to bump, carrying a
|
||||
version AND a changelog in one string:
|
||||
|
||||
VERSION = "2026-07-17.1 · idle model-unload: after ~5 min idle ..."
|
||||
|
||||
Nobody bumped it. The September image printed the identical string to the July
|
||||
one, so the one surface that was supposed to answer *"did my pull work?"*
|
||||
answered *"2026-07-17.1"* either way. An artifact that cannot identify itself
|
||||
is worse than one that says nothing, because the stale value reads as an
|
||||
answer.
|
||||
|
||||
The values are now derived by `scripts/artifacts.sh` from the commit its
|
||||
shipped files last changed in — the same derivation the web image has used
|
||||
since milestone 313, and the same one the reuse check already ran for the
|
||||
agent and discarded.
|
||||
|
||||
## Three values, never folded together (rule 149)
|
||||
|
||||
* `FC_VERSION` — the NAME, `YYYY.MM.DD.HHMM` UTC, derived from COMMIT time.
|
||||
For people to read and quote. Identical on `dev` and `main` for the same
|
||||
source, which is the property that makes "am I running the same code as
|
||||
production?" answerable at a glance.
|
||||
* `FC_CHANNEL` — a SIBLING field, never a suffix inside the name.
|
||||
* `FC_REVISION` — the 12-char commit sha, the artifact's IDENTITY. This is
|
||||
what the reuse check already keys on as the `fc.revision` image label.
|
||||
|
||||
**Absent rather than empty when unknown.** A locally built image has no
|
||||
stamp, and neither does any image predating this module. One spelling of
|
||||
"cannot say" instead of two.
|
||||
"""
|
||||
|
||||
import os
|
||||
|
||||
FC_VERSION = os.environ.get("FC_VERSION", "").strip()
|
||||
FC_CHANNEL = os.environ.get("FC_CHANNEL", "").strip()
|
||||
FC_REVISION = os.environ.get("FC_REVISION", "").strip()
|
||||
|
||||
|
||||
def display_version() -> str:
|
||||
"""The build, as a line for a human: `2026.09.24.1052 (dev)`.
|
||||
|
||||
`unknown` rather than a blank when unstamped — an empty slot in the meta
|
||||
line reads as "no version", which is a different and false claim from "this
|
||||
build does not carry one".
|
||||
"""
|
||||
if not FC_VERSION:
|
||||
return "unknown"
|
||||
return f"{FC_VERSION} ({FC_CHANNEL})" if FC_CHANNEL else FC_VERSION
|
||||
|
||||
|
||||
def build_id() -> str:
|
||||
"""The token the control page compares against `/status` to notice it is
|
||||
showing a CACHED page from a previous build.
|
||||
|
||||
Deliberately NOT `display_version()`. That is the value for reading; this
|
||||
is the value for deciding, and folding the two is what rule 149 is about.
|
||||
The revision is the better discriminator of the two — two builds of the
|
||||
same commit ARE the same agent and should not prompt a reload, and two
|
||||
different commits always differ here even when they land in the same
|
||||
minute and derive one version name.
|
||||
|
||||
A locally built image falls through to a constant, so the reload banner
|
||||
cannot fire for it. That is honest rather than a gap: nothing in an
|
||||
unstamped image knows what source it was built from, and a per-process
|
||||
nonce would make every ordinary container RESTART claim a new version had
|
||||
arrived — a false positive on the exact surface the banner exists to keep
|
||||
trustworthy.
|
||||
"""
|
||||
return FC_REVISION or FC_VERSION or "local"
|
||||
+22
-9
@@ -55,20 +55,33 @@ WEB_PATHS='Dockerfile requirements.txt requirements-ml.txt backend alembic alemb
|
||||
# fc_agent only. agent/README.md, agent/docker-compose.yml and agent/ruff.toml
|
||||
# live in the directory but never reach the image, so they must not re-version
|
||||
# it: this is deliberately NOT `agent/`.
|
||||
#
|
||||
# $DERIVER is appended in cmd_paths, not here, so the two artifacts that stamp
|
||||
# a version pick it up the same way rather than one of them carrying it in its
|
||||
# literal — see DERIVER below for why it belongs to both.
|
||||
AGENT_PATHS='agent/Dockerfile agent/requirements.txt agent/fc_agent'
|
||||
|
||||
# This file. It is copied into no image and it is still part of what the web
|
||||
# image is built from, because it DECIDES the FC_VERSION baked into that image
|
||||
# (#3202). Same finding as #3156 about packaging.sh, one level up.
|
||||
#
|
||||
# Why web and nothing else. Every artifact stamps `fc.revision`, but only web
|
||||
# also stamps a version (build.yml line ~488 feeds `version web` to the
|
||||
# FC_VERSION build arg; the agent asks for `revision` alone, and the
|
||||
# extension takes its version from packaging.sh). For a revision-only artifact
|
||||
# this file needs no entry: any change to how the revision is COMPUTED changes
|
||||
# the derived value, which then disagrees with the label on the published image
|
||||
# and forces a rebuild. That mechanism is self-correcting because it compares
|
||||
# against a string stamped into a real artifact.
|
||||
# Web AND agent, since 2026-09-24. Both stamp a VERSION into the image they
|
||||
# build (FC_VERSION), and the extension takes its version from packaging.sh.
|
||||
#
|
||||
# The agent was deliberately absent from this list until then, and the reason
|
||||
# it was correct to be absent is the reason it no longer is: it asked this
|
||||
# script for `revision` alone. For a revision-only artifact this file needs no
|
||||
# entry — any change to how the revision is COMPUTED changes the derived value,
|
||||
# which then disagrees with the label on the published image and forces a
|
||||
# rebuild. That mechanism is self-correcting because it compares against a
|
||||
# string stamped into a real artifact.
|
||||
#
|
||||
# Giving the agent a self-reported version took that backstop away from it, so
|
||||
# it inherits web's exposure and has to be listed here in the SAME change that
|
||||
# introduced the version. Left out, the failure is the one recorded below: a
|
||||
# change to `cmd_version` alone would move no revision, the reuse check would
|
||||
# hit, the build would be skipped, and the published agent would go on
|
||||
# reporting the old format — with nothing anywhere to disagree with it.
|
||||
#
|
||||
# The version is compared against nothing, so it has no such backstop. Before
|
||||
# this entry, a change to cmd_version alone left every artifact's revision
|
||||
@@ -100,7 +113,7 @@ ext_paths() {
|
||||
cmd_paths() {
|
||||
case "$1" in
|
||||
web) echo "$WEB_PATHS $DERIVER $(ext_paths)" ;;
|
||||
agent) echo "$AGENT_PATHS" ;;
|
||||
agent) echo "$AGENT_PATHS $DERIVER" ;;
|
||||
extension) ext_paths ;;
|
||||
*) usage ;;
|
||||
esac
|
||||
|
||||
@@ -0,0 +1,120 @@
|
||||
"""The agent's self-report: a derived version, and a build id kept apart from it.
|
||||
|
||||
The agent's version used to be a literal in `app.py` that an author was asked
|
||||
to bump on every change. Nobody did, so the September image printed the same
|
||||
`2026-07-17.1` as the July one — the one surface meant to answer *"did my pull
|
||||
work?"* gave the same answer either way.
|
||||
|
||||
These pin the two properties that make the replacement worth having: the
|
||||
displayed string degrades honestly when a build carries no stamp, and the value
|
||||
the page COMPARES is not the value it SHOWS (rule 149).
|
||||
|
||||
`build_info` is stdlib-only on purpose, so the suite can import it — `app` is
|
||||
unimportable here (torch, transformers, ultralytics are not in the CI image,
|
||||
which is why build.yml lints the agent and then only `compileall`s it).
|
||||
"""
|
||||
|
||||
from __future__ import annotations
|
||||
|
||||
from pathlib import Path
|
||||
|
||||
import pytest
|
||||
|
||||
from agent.fc_agent import build_info
|
||||
|
||||
APP = Path(__file__).resolve().parents[1] / "agent" / "fc_agent" / "app.py"
|
||||
|
||||
|
||||
def _stamp(monkeypatch, *, version="", channel="", revision=""):
|
||||
monkeypatch.setattr(build_info, "FC_VERSION", version)
|
||||
monkeypatch.setattr(build_info, "FC_CHANNEL", channel)
|
||||
monkeypatch.setattr(build_info, "FC_REVISION", revision)
|
||||
|
||||
|
||||
# --- what a person reads -----------------------------------------------------
|
||||
|
||||
|
||||
def test_an_unstamped_build_says_unknown_rather_than_nothing(monkeypatch):
|
||||
"""A blank in the meta line reads as "no version", which is a different and
|
||||
false claim from "this build does not carry one". Same posture the web
|
||||
image's build_info takes, for the same reason."""
|
||||
_stamp(monkeypatch)
|
||||
|
||||
assert build_info.display_version() == "unknown"
|
||||
|
||||
|
||||
def test_the_channel_rides_beside_the_version_not_inside_it(monkeypatch):
|
||||
"""Rule 149. The channel is a sibling field; a `-dev` folded into the name
|
||||
is issue #2993's exact failure. Rendering it in parentheses is display, not
|
||||
encoding — `FC_VERSION` itself never carries it, which the next test is
|
||||
what actually holds."""
|
||||
_stamp(monkeypatch, version="2026.09.24.1052", channel="dev")
|
||||
|
||||
assert build_info.display_version() == "2026.09.24.1052 (dev)"
|
||||
|
||||
|
||||
def test_a_version_with_no_channel_is_the_bare_version(monkeypatch):
|
||||
_stamp(monkeypatch, version="2026.09.24.1052")
|
||||
|
||||
assert build_info.display_version() == "2026.09.24.1052"
|
||||
|
||||
|
||||
# --- what the page decides on ------------------------------------------------
|
||||
|
||||
|
||||
def test_the_build_id_prefers_the_revision(monkeypatch):
|
||||
"""Two builds of the same commit ARE the same agent and must not prompt a
|
||||
reload; two different commits always differ here, even when they land in
|
||||
the same minute and derive one version name. The revision is the only one
|
||||
of the three that is true of the content rather than of the clock."""
|
||||
_stamp(monkeypatch, version="2026.09.24.1052", revision="a3071a754907")
|
||||
|
||||
assert build_info.build_id() == "a3071a754907"
|
||||
|
||||
|
||||
def test_the_build_id_falls_back_to_the_version_then_to_a_constant(monkeypatch):
|
||||
_stamp(monkeypatch, version="2026.09.24.1052")
|
||||
assert build_info.build_id() == "2026.09.24.1052"
|
||||
|
||||
_stamp(monkeypatch)
|
||||
# A constant, deliberately — see build_info.build_id. A per-process nonce
|
||||
# would make every ordinary container RESTART claim a new version had
|
||||
# arrived, a false positive on the surface the banner exists to keep
|
||||
# trustworthy.
|
||||
assert build_info.build_id() == "local"
|
||||
|
||||
|
||||
def test_the_displayed_string_is_never_the_compared_one(monkeypatch):
|
||||
"""The property both of the above serve, stated once on its own. Folding
|
||||
these together is how the version acquires a second job and then cannot be
|
||||
changed without breaking the reload banner."""
|
||||
_stamp(monkeypatch, version="2026.09.24.1052", channel="dev",
|
||||
revision="a3071a754907")
|
||||
|
||||
assert build_info.display_version() != build_info.build_id()
|
||||
|
||||
|
||||
# --- the page wires them to the right places ---------------------------------
|
||||
#
|
||||
# Read as SOURCE, not imported: `app` pulls the agent's GPU stack. Asserting on
|
||||
# what is PRESENT rather than on what is absent, deliberately — a guard that
|
||||
# greps for the absence of a pattern trips over the comment explaining why the
|
||||
# pattern is gone (lesson #4365), and app.py's new comment names the old
|
||||
# literal it replaced.
|
||||
|
||||
|
||||
@pytest.mark.parametrize(
|
||||
"marker, why",
|
||||
[
|
||||
(
|
||||
'id=build>__VERSION__',
|
||||
"the meta line must show the human-readable version",
|
||||
),
|
||||
(
|
||||
'const PAGE_BUILD="__BUILD_ID__"',
|
||||
"the staleness check must compare the build id, not the version",
|
||||
),
|
||||
],
|
||||
)
|
||||
def test_the_control_page_shows_one_value_and_compares_the_other(marker, why):
|
||||
assert marker in APP.read_text(), why
|
||||
@@ -131,14 +131,21 @@ DERIVERS = [
|
||||
# so the reuse path republishes the old image and the fresh signature is
|
||||
# orphaned. Guarded for web too, since web bundles what the extension makes.
|
||||
("extension/scripts/packaging.sh", ("extension", "web")),
|
||||
# artifacts.sh decides the FC_VERSION baked into the web image (#3202).
|
||||
# Web only, and deliberately: the agent asks this script for `revision`
|
||||
# alone, so they are covered by the self-correcting path above, and the
|
||||
# extension takes its version from packaging.sh. Milestone 318 step 5 is
|
||||
# the worked instance — b3989d0 and 5771fd5 share revision fb2c4d5b80be
|
||||
# while the version moved 2026.8.28.1249 -> 2026.08.28.1249. It was
|
||||
# harmless only because FC_VERSION did not exist until one commit later.
|
||||
("scripts/artifacts.sh", ("web",)),
|
||||
# artifacts.sh decides the FC_VERSION baked into the web image (#3202)
|
||||
# and, since 2026-09-24, the agent image too.
|
||||
#
|
||||
# It was web-only before that, and correctly so: the agent asked this
|
||||
# script for `revision` alone, which the self-correcting path above
|
||||
# covers. Giving the agent a self-reported version is what moved it into
|
||||
# this list, and the two had to happen in the SAME change — a version with
|
||||
# no deriver entry is the exposure this list exists for, not a smaller
|
||||
# version of it. The extension still takes its version from packaging.sh.
|
||||
#
|
||||
# Milestone 318 step 5 is the worked instance — b3989d0 and 5771fd5 share
|
||||
# revision fb2c4d5b80be while the version moved 2026.8.28.1249 ->
|
||||
# 2026.08.28.1249. It was harmless only because FC_VERSION did not exist
|
||||
# until one commit later.
|
||||
("scripts/artifacts.sh", ("web", "agent")),
|
||||
]
|
||||
|
||||
|
||||
|
||||
Reference in New Issue
Block a user