From 7a5ddcc367e0d1b82931e97f0692497b7b5145b2 Mon Sep 17 00:00:00 2001
From: quao627
Date: Sun, 13 Sep 2026 03:05:03 -0400
Subject: [PATCH 01/28] feat(page): show the proposer's design and review, the
floor verdict and refused requires
The release page renders, when the row carries them, a Design section after
Why (proposal_notes.design), a Review section after What changed (the
review verdict, the covered and uncovered points, and the variables a
written extension reads that no requires item names), the floor gate's
passed, failed, floor_score and gate_sides in Verdict, and the items the
step refused (training_request.refused_requires and
proposal_notes.refused_requires) with their reason under Setup. A row
without them renders as before. The HTTP API reference documents the new
step metrics and the page's sections.
---
docs/reference/http-api.rst | 59 ++++++---
reef/service/release_page.py | 131 +++++++++++++++++--
tests/reef_service/test_release_page.py | 165 ++++++++++++++++++++++++
3 files changed, 326 insertions(+), 29 deletions(-)
diff --git a/docs/reference/http-api.rst b/docs/reference/http-api.rst
index f8f85eb02..7c15c45b6 100644
--- a/docs/reference/http-api.rst
+++ b/docs/reference/http-api.rst
@@ -61,8 +61,8 @@ Routes
+--------------------------------------------------------+---------------------------------------------------+
| ``GET /reef/harness/releases`` | the harness release catalog, oldest first |
+--------------------------------------------------------+---------------------------------------------------+
-| ``GET /reef/harness/releases/{step}/page`` | one HTML page per catalog step: why, what |
-| | changed, verdict, setup, chain |
+| ``GET /reef/harness/releases/{step}/page`` | one HTML page per catalog step: why, design, what |
+| | changed, review, verdict, setup, chain |
+--------------------------------------------------------+---------------------------------------------------+
| ``GET /reef/harness/releases/{step}/records`` | retained raw step file inventory or file body |
+--------------------------------------------------------+---------------------------------------------------+
@@ -197,6 +197,21 @@ that answered a request carries, for example:
"session": "session-1", "release_id": "release-1",
"requires": [{"name": "TWILIO_SID", "kind": "env", "check": "TWILIO_SID"}]}}
+When the backend dropped an item the method added (malformed, or credential
+or directive shaped), ``training_request.refused_requires`` lists each as
+``{"item": , "reason": "..."}``; the key is absent when
+nothing was dropped. A method may also record notes beside its proposal;
+they land under ``proposal_notes``, a JSON mapping the backend writes as
+given and never reads, present only when non-empty. Reefine writes
+``design`` (the proposer's plan, a few sentences), ``review``
+(``{"verdict": "complete" | "partial", "covered": [...], "uncovered": [...]}``,
+the proposer's own reading of its entries against the request; absent when
+the review call failed), ``refused_requires`` (the items the method itself
+dropped, in the same ``{item, reason}`` shape) and ``undeclared_env`` (the
+variables a written extension reads through ``process.env`` that no
+``requires`` item names; nothing adds them, the version page shows them).
+Other methods may write other keys.
+
Supply ``agent_record_id`` to retry safely: an identical request is accepted
without another step, including after record compaction; reusing the id with
different content returns HTTP 409. Without it, each submission gets a fresh
@@ -593,8 +608,10 @@ that error and leave mode switching to the caller.
The existing trainer delivers the request to a proposer that explicitly
accepts ``requests``, then evaluates and publishes under the same policy as
automatic evolution. Its commit metrics carry ``training_request:
-{id, text, session, release_id, requires}``, visible through the release
-catalog.
+{id, text, session, release_id, requires}`` (with ``refused_requires`` when
+the backend dropped an item the method added) and, for a method that records
+notes beside its proposal, ``proposal_notes``, both visible through the
+release catalog.
Rollback
~~~~~~~~
@@ -629,21 +646,33 @@ page (``text/html``, no asset, its data inline) for one catalog row. ``step``
is the row's position in ``GET /reef/harness/releases`` oldest first, the
creation row being 0, which is the commit step: a rejected step publishes
nothing and its row carries the head's release id, so the step is what names
-it. The page has five sections in this order: Why (the request the step read,
-else the claimed proposal's reason, else a failure in the batch), What changed
-(the step's mutations; an extension's file as text for a create, and for an
-update a line diff against the release the candidate ran on when that release
-is restorable, else the new text), Verdict (the verdict with ``selected``,
-``wins``, ``losses``, ``ties``, ``current_score``, ``candidate_score``,
-``episode_failures``, and the step record directory when
+it. The page has up to seven sections in this order: Why (the request the
+step read, else the claimed proposal's reason, else a failure in the batch),
+Design (the proposer's plan, ``proposal_notes.design``; only when the method
+recorded one), What changed (the step's mutations; an extension's file as
+text for a create, and for an update a line diff against the release the
+candidate ran on when that release is restorable, else the new text), Review
+(``proposal_notes.review``: the proposer's verdict on its entries against the
+request, ``complete`` or ``partial``, then the points it covered and the ones
+it left uncovered, then one line naming ``proposal_notes.undeclared_env``,
+the variables a written extension reads that no ``requires`` item names;
+only when the row carries a review or that list), Verdict (the verdict with
+``selected``, ``wins``, ``losses``, ``ties``, ``passed``, ``failed``,
+``floor_score``, ``gate_sides``, ``current_score``, ``candidate_score`` and
+``episode_failures``, each when the row carries it, so a ``floor`` gate,
+which runs no current side, shows ``passed``, ``failed`` and ``floor_score``
+and no ``current_score``, and the step record directory when
``evolution.step_record_dir`` is set), Setup (the request's ``requires`` with
name, kind and check, then the items the release carries from earlier steps
in its chain, the same union the install script and ``reef- setup``
read; a rejected or skipped row lists only its own items, since its release
-id is the head's; nothing when both are empty) and Chain (the parent
-release, this release, and its children: the steps gated on it, won, lost or
-pending, and a promote or rollback made on it; a rejected or skipped step
-published nothing, so its Chain names the head it ran on and no children).
+id is the head's; then, under "refused by the step", the items the step
+dropped from ``training_request.refused_requires`` and
+``proposal_notes.refused_requires``, each as written with its reason;
+nothing when all are empty) and Chain (the parent release, this release,
+and its children: the steps gated on it, won, lost or pending, and a promote
+or rollback made on it; a rejected or skipped step published nothing, so its
+Chain names the head it ran on and no children).
The line under the title marks the served head as ``current``: the newest
row that is neither pending nor a rejected or skipped step. A pending row
that a later ``promote`` row names in ``rollback_target_release_id`` reads
diff --git a/reef/service/release_page.py b/reef/service/release_page.py
index 7bdcdbf35..fe91bf2ab 100644
--- a/reef/service/release_page.py
+++ b/reef/service/release_page.py
@@ -1,11 +1,14 @@
"""One HTML page per catalog step: why the version exists, what it changed, the gate's verdict, its setup, its chain.
``GET /reef/harness/releases/{step}/page`` builds it from the releases row
-plus, for an extension update, the file the release replaced. The page loads
-no asset and carries its data inline, so one curl with the scenario header is
-the whole read. The step is the row's position in the catalog oldest first,
-the creation row being 0: a rejected step publishes nothing and its row
-carries the head's release id, so only the step names it.
+plus, for an extension update, the file the release replaced. A step whose
+method recorded ``proposal_notes`` (Reefine's design, review, refused
+requires and undeclared variables) also gets a Design section after Why and
+a Review section after What changed. The page loads no asset and carries its
+data inline, so one curl with the scenario header is the whole read. The
+step is the row's position in the catalog oldest first, the creation row
+being 0: a rejected step publishes nothing and its row carries the head's
+release id, so only the step names it.
"""
from __future__ import annotations
@@ -18,12 +21,17 @@
from reef.core.requirements import required_by
-#: The gate numbers the Verdict section lists, in this order, when the row carries them.
+#: The gate numbers the Verdict section lists, in this order, when the row carries them: a comparison writes
+#: wins, losses and ties; a floor writes passed, failed and floor_score, and gate_sides when it ran one side only.
VERDICT_FIELDS = (
"selected",
"wins",
"losses",
"ties",
+ "passed",
+ "failed",
+ "floor_score",
+ "gate_sides",
"current_score",
"candidate_score",
"episode_failures",
@@ -58,8 +66,9 @@ def _gate_tokens(metrics: Mapping[str, Any]) -> tuple[int, int] | None:
h1{font-size:1.5rem;margin:0 0 .25rem}h2{font-size:1.05rem;margin:2rem 0 .75rem;text-transform:uppercase;letter-spacing:.06em;color:var(--mute)}
h3{font-size:.95rem;margin:1rem 0 .5rem;font-weight:600}
.sub{color:var(--mute);margin:0 0 1rem}.id{font-family:ui-monospace,SFMono-Regular,Menlo,monospace;font-size:.85rem}
+.text{white-space:pre-wrap}
.tag{display:inline-block;font-size:.72rem;padding:0 .35rem;border:1px solid var(--line);border-radius:3px;margin-right:.25rem;color:var(--mute)}
-.selected,.promoted{color:var(--good)}.rejected{color:var(--bad)}.pending,.skipped{color:var(--warn)}
+.selected,.promoted,.complete{color:var(--good)}.rejected{color:var(--bad)}.pending,.skipped,.partial{color:var(--warn)}
pre{white-space:pre-wrap;word-break:break-word;background:var(--code);padding:.5rem;border-radius:4px;max-height:32rem;overflow:auto;font-size:.8rem}
.add{color:var(--good)}.del{color:var(--bad)}.hunk{color:var(--mute)}
table{border-collapse:collapse;width:100%;font-size:.85rem}th,td{text-align:left;padding:.3rem .5rem;border-bottom:1px solid var(--line);vertical-align:top}
@@ -76,6 +85,19 @@ def _short(release_id: Any) -> str:
return str(release_id)[:8] if release_id else "-"
+def _notes(metrics: Mapping[str, Any]) -> Mapping[str, Any]:
+ """What the method recorded beside its proposal, ``proposal_notes``; empty when the step carries none."""
+ notes = metrics.get("proposal_notes")
+ return notes if isinstance(notes, Mapping) else {}
+
+
+def _strings(value: Any) -> list[str]:
+ """The items of a JSON list as text, in order; none when ``value`` is not a list."""
+ if isinstance(value, Sequence) and not isinstance(value, str):
+ return [item if isinstance(item, str) else json.dumps(item, sort_keys=True) for item in value]
+ return []
+
+
def mutations_of(metrics: Mapping[str, Any] | None) -> list[Mapping[str, Any]]:
"""The step's mutations: ``mutation`` for one, ``mutations`` for a composite, none for a skip or a recheck."""
if not metrics:
@@ -158,7 +180,7 @@ def _why(row: Mapping[str, Any], metrics: Mapping[str, Any]) -> str:
request = metrics.get("training_request")
if isinstance(request, Mapping) and request.get("text"):
return (
- f"
{_esc(request['text'])}
"
+ f"
{_esc(request['text'])}
"
f"
request {_esc(request.get('id'))} from session "
f"{_esc(request.get('session'))} on release "
f"{_esc(request.get('release_id'))}
"
+def _design(metrics: Mapping[str, Any]) -> str:
+ """The Design section: the proposer's plan for the request, when the method recorded ``proposal_notes.design``."""
+ design = _notes(metrics).get("design")
+ if not isinstance(design, str) or not design.strip():
+ return ""
+ return f'
'
+def _listed(items: Sequence[str], empty: str) -> str:
+ if not items:
+ return f'
{_esc(empty)}
'
+ return "
" + "".join(f"
{_esc(item)}
" for item in items) + "
"
+
+
+def _review(metrics: Mapping[str, Any]) -> str:
+ """The Review section: the proposer's reading of its entries against the request, then what it left undeclared.
+
+ ``proposal_notes.review`` is absent when the method's review call failed;
+ the ``undeclared_env`` line shows all the same, being the warning the
+ person needs. Empty when the step recorded neither."""
+ notes = _notes(metrics)
+ review = notes.get("review")
+ undeclared = _strings(notes.get("undeclared_env"))
+ if not isinstance(review, Mapping) and not undeclared:
+ return ""
+ parts = []
+ if isinstance(review, Mapping):
+ verdict = _span(str(review.get("verdict") or "unknown"))
+ parts.append(f"
the proposer's review of its entries against the request: {verdict}
")
+ parts.append("
covered
" + _listed(_strings(review.get("covered")), "nothing listed as covered"))
+ parts.append("
uncovered
" + _listed(_strings(review.get("uncovered")), "nothing left uncovered"))
+ else:
+ parts.append('
no review on record
')
+ if undeclared:
+ parts.append(
+ "
the extension reads these variables and no requires item names them: "
+ f'{_esc(", ".join(undeclared))}
'
+ )
+ return "
Review
\n" + "".join(parts) + "\n"
+
+
def _verdict(row: Mapping[str, Any], metrics: Mapping[str, Any], rows: Sequence[Mapping[str, Any]]) -> str:
verdict = verdict_of(row, rows)
notes = {
@@ -278,7 +341,12 @@ def _verdict(row: Mapping[str, Any], metrics: Mapping[str, Any], rows: Sequence[
for field in VERDICT_FIELDS:
if field in metrics:
value = metrics[field]
- shown = json.dumps(value) if isinstance(value, bool) else str(value)
+ if isinstance(value, bool):
+ shown = json.dumps(value)
+ elif isinstance(value, Sequence) and not isinstance(value, str):
+ shown = ", ".join(_strings(value))
+ else:
+ shown = str(value)
lines.append(f"
{_esc(field.replace('_', ' '))}
{_esc(shown)}
")
gate_tokens = _gate_tokens(metrics)
if gate_tokens is not None:
@@ -300,6 +368,27 @@ def _requires_table(items: Sequence[Mapping[str, Any]]) -> str:
return f"
name
kind
check
{rows}
"
+def _refused_table(entries: Sequence[Mapping[str, Any]]) -> str:
+ """The ``refused_requires`` records: each item as it was written (name, kind, check) and why it was dropped."""
+ rows = []
+ for entry in entries:
+ # The backend and the method record {item, reason}; a record without "item" is read as the item itself.
+ item = entry.get("item", entry)
+ if isinstance(item, Mapping):
+ cells = (item.get("name"), item.get("kind"), item.get("check") or "")
+ else:
+ # A malformed item need not be an object at all; its JSON stands where the name would.
+ cells = (json.dumps(item, sort_keys=True), "", "")
+ rows.append(
+ f'
{_esc(cells[0])}
{_esc(cells[1])}
{_esc(cells[2])}
'
+ f"
{_esc(entry.get('reason'))}
"
+ )
+ return (
+ "
name
kind
check
reason
"
+ f"{''.join(rows)}
"
+ )
+
+
def _setup(row: Mapping[str, Any], metrics: Mapping[str, Any], rows: Sequence[Mapping[str, Any]]) -> str:
"""The step's own ``training_request.requires`` items, then what its release carries from earlier steps.
@@ -307,21 +396,32 @@ def _setup(row: Mapping[str, Any], metrics: Mapping[str, Any], rows: Sequence[Ma
the union over the release's chain (``required_by``), so the page lists
the same items split by the step that named them. A rejected or skipped
row carries the head's id and published nothing, so only its own items
- show."""
+ show. The items the step dropped, the backend's under
+ ``training_request.refused_requires`` and the method's under
+ ``proposal_notes.refused_requires``, close the section with their reason."""
request = metrics.get("training_request")
- requires = request.get("requires") if isinstance(request, Mapping) else None
+ request = request if isinstance(request, Mapping) else {}
+ requires = request.get("requires")
own = [item for item in requires if isinstance(item, Mapping)] if isinstance(requires, Sequence) else []
carried: list[Mapping[str, Any]] = []
if verdict_of(row) not in ("rejected", "skipped"):
names = {item.get("name") for item in own}
carried = [item for item in required_by(rows, row.get("release_id")) if item.get("name") not in names]
+ refused = [
+ entry
+ for source in (request.get("refused_requires"), _notes(metrics).get("refused_requires"))
+ if isinstance(source, Sequence) and not isinstance(source, str)
+ for entry in source
+ if isinstance(entry, Mapping)
+ ]
+ tail = f"
refused by the step
{_refused_table(refused)}" if refused else ""
if not own and not carried:
- return '
this step names nothing to set up
'
+ return '
this step names nothing to set up
' + tail
parts = [_requires_table(own) if own else '
this step names nothing of its own
']
if carried:
parts.append(f"
carried from earlier steps
{_requires_table(carried)}")
parts.append('
reef-pi setup lists these and runs a check only after you confirm it
')
- return "".join(parts)
+ return "".join(parts) + tail
def _ran_on(other: Mapping[str, Any], release_id: Any) -> bool:
@@ -378,7 +478,8 @@ def build_release_page(
``before_entries`` and ``before_files`` describe the release an update is
read against (see ``before_release_id``); ``node_paths`` is the adapter's
render template per kind, which names an extension's file. Without them an
- extension update shows its new text instead of a diff. Pure ASCII out:
+ extension update shows its new text instead of a diff. Design and Review
+ appear only when the row's ``proposal_notes`` carry them. Pure ASCII out:
other characters leave as numeric references.
"""
row = rows[step]
@@ -399,7 +500,9 @@ def build_release_page(
f"{title}\n\n\n"
f'
' in setup
assert "reef-pi setup" in setup and "carried from earlier steps" not in setup
+ assert "refused by the step" not in setup
+ # No proposal notes on the row: no Design and no Review section.
+ assert _sections(page) == ["Why", "What changed", "Verdict", "Setup", "Chain"]
data = _data(page)
assert data["metrics"]["training_request"]["text"] == "caf\u00e9 "
assert data["metrics"]["training_request"]["requires"][1]["check"] == "osascript -e '1 < 2'"
@@ -591,3 +594,165 @@ def test_the_diff_colours_lines_by_position_so_a_plus_plus_line_is_an_addition()
assert '--- pi-agent/extensions/hello.ts (rel-0)' in changed
assert '+++ pi-agent/extensions/hello.ts (rel-1)' in changed
assert '---n;' in changed and '+++n;' in changed
+
+
+def test_the_page_shows_the_proposers_design_and_review_and_escapes_them() -> None:
+ creation = {"release_id": "rel-0", "parent_release_id": None, "operation": "creation"}
+ notes = {
+ "design": "Add an command.\n\nA rule alone would assume the state holds.",
+ "review": {
+ "verdict": "partial",
+ "covered": ["the /away command toggles the state", "the rule reads a < b"],
+ "uncovered": ["no notice when the state clears"],
+ },
+ "undeclared_env": ["SLACK_WEBHOOK", "XAdd an <away> command.\n\nA rule alone would assume the state holds.
' in design
+ review = _section(page, "Review")
+ assert 'the proposer\'s review of its entries against the request: partial' in review
+ assert (
+ "
covered
the /away command toggles the state
the rule reads a < b
"
+ in review
+ )
+ assert "
uncovered
no <b>notice</b> when the state clears
" in review
+ assert (
+ "the extension reads these variables and no requires item names them: "
+ 'SLACK_WEBHOOK, X<Y' in review
+ )
+ assert "" not in page and "notice" not in page and "Xcomplete' in review
+ assert '
uncovered
nothing left uncovered
' in review
+ assert "no requires item names them" not in review
+ # The review call failed: no verdict on record, but the undeclared variables still show.
+ failed = {"design": "One command.", "undeclared_env": ["TWILIO_SID"]}
+ page = build_release_page(1, [creation, {**row, "metrics": {**row["metrics"], "proposal_notes": failed}}])
+ assert _sections(page) == ["Why", "Design", "What changed", "Review", "Verdict", "Setup", "Chain"]
+ review = _section(page, "Review")
+ assert '
' in verdict and "candidate won 2 of 3" in verdict
setup = _section(page, "Setup")
- assert '
SLACK_WEBHOOK
env
SLACK_WEBHOOK
' in setup
+ assert "
name
kind
check
prompt
" in setup
+ assert '
SLACK_WEBHOOK
env
SLACK_WEBHOOK
' in setup
assert (
- '
notifications
permission
osascript -e '1 < 2'
'
- in setup
+ '
notifications
permission
osascript -e '1 < 2'
'
+ "
Allow <notifications> & "more"
" in setup
)
- assert '
calendar
service
' in setup
+ assert '
calendar
service
' in setup
assert "reef-pi setup" in setup and "carried from earlier steps" not in setup
assert "refused by the step" not in setup
# No proposal notes on the row: no Design and no Review section.
@@ -418,7 +424,7 @@ def test_the_page_module_is_ascii_and_the_builder_escapes_every_angle_bracket()
def test_the_setup_section_splits_the_steps_own_items_from_what_the_chain_carries() -> None:
"""The same union the install script reads (required_by), shown by the step that named each item."""
twilio = {"name": "TWILIO_SID", "kind": "env", "check": "TWILIO_SID"}
- notify = {"name": "notify", "kind": "permission", "check": "test -d /"}
+ notify = {"name": "notify", "kind": "permission", "check": "test -d /", "prompt": "Allow notifications"}
never = {"name": "never", "kind": "service"}
creation = {"release_id": "rel-0", "parent_release_id": None, "operation": "creation"}
first = {
@@ -448,14 +454,14 @@ def test_the_setup_section_splits_the_steps_own_items_from_what_the_chain_carrie
"rollback_target_release_id": "rel-2",
}
rows = [creation, first, rejected, second, promote]
- twilio_row = '
TWILIO_SID
env
TWILIO_SID
'
- notify_row = '
notify
permission
test -d /
'
+ twilio_row = '
TWILIO_SID
env
TWILIO_SID
'
+ notify_row = '
notify
permission
test -d /
Allow notifications
'
assert "nothing to set up" in _section(build_release_page(0, rows), "Setup")
own_only = _section(build_release_page(1, rows), "Setup")
assert twilio_row in own_only and "carried from earlier steps" not in own_only
candidate = _section(build_release_page(2, rows), "Setup")
- assert '
never
service
' in candidate
+ assert '
never
service
' in candidate
assert "TWILIO_SID" not in candidate and "carried from earlier steps" not in candidate
pending = _section(build_release_page(3, rows), "Setup")
own, _, carried = pending.partition("
carried from earlier steps
")
@@ -746,7 +752,10 @@ def test_the_setup_section_lists_the_requires_the_step_refused_with_their_reason
{"item": "SLACK_WEBHOOK", "reason": "requires[0] must be an object with a name and a kind"},
]
by_method = [
- {"item": {"name": "phone", "kind": "sms"}, "reason": "kind must be one of ('permission', 'env', 'service')"}
+ {
+ "item": {"name": "phone", "kind": "sms", "prompt": "The number to text, with the code"},
+ "reason": "kind must be one of ('permission', 'env', 'service')",
+ }
]
row = {
"release_id": "rel-1",
@@ -767,18 +776,19 @@ def test_the_setup_section_lists_the_requires_the_step_refused_with_their_reason
page.encode("ascii")
setup = _section(page, "Setup")
own, _, refused = setup.partition("
refused by the step
")
- assert '
TWILIO_SID
env
TWILIO_SID
' in own and "reef-pi setup" in own
- assert "
name
kind
check
reason
" in refused
+ assert '
TWILIO_SID
env
TWILIO_SID
' in own
+ assert "reef-pi setup" in own
+ assert "
name
kind
check
prompt
reason
" in refused
assert (
- '
bad name
env
test -n "$X" && echo 1 < 2
'
+ '
bad name
env
test -n "$X" && echo 1 < 2
'
"
requires[0].name must be a non-empty string matching ^[A-Za-z0-9][A-Za-z0-9._-]*$
" in refused
)
assert (
- '
"SLACK_WEBHOOK"
'
+ '
"SLACK_WEBHOOK"
'
"
requires[0] must be an object with a name and a kind
" in refused
)
assert (
- '
phone
sms
'
+ '
phone
sms
The number to text, with the <country> code
'
"
kind must be one of ('permission', 'env', 'service')
" in refused
)
# The backend's records first, then the method's.
diff --git a/tests/reef_service/test_request_page.py b/tests/reef_service/test_request_page.py
index 645c356db..5324a546e 100644
--- a/tests/reef_service/test_request_page.py
+++ b/tests/reef_service/test_request_page.py
@@ -232,7 +232,10 @@ def test_a_skipped_request_shows_why_the_proposer_produced_nothing_and_what_the_
def test_the_page_module_is_ascii_and_the_builder_escapes_the_request_the_notes_and_the_link() -> None:
MODULE.read_text(encoding="utf-8").encode("ascii")
text = 'text me & "quote" café'
- requires = [{"name": "TWILIO_SID", "kind": "env", "check": "TWILIO_SID"}, {"name": "", "kind": "service"}]
+ requires = [
+ {"name": "TWILIO_SID", "kind": "env", "check": "TWILIO_SID"},
+ {"name": "", "kind": "service", "prompt": "Sign in to "},
+ ]
notes = {"failure": "failed", "review": {"verdict": "partial", "covered": [], "uncovered": ["off"]}}
row = _row(_answered(skipped="no proposal", proposal_notes=notes), release_id="rel-0")
page = build_request_page(
@@ -244,7 +247,13 @@ def test_the_page_module_is_ascii_and_the_builder_escapes_the_request_the_notes_
page.encode("ascii")
assert " & "quote" café'
+ text = 'text me & "quote" caf\u00e9'
requires = [
{"name": "TWILIO_SID", "kind": "env", "check": "TWILIO_SID"},
{"name": "", "kind": "service", "prompt": "Sign in to "},
@@ -245,17 +247,17 @@ def test_the_page_module_is_ascii_and_the_builder_escapes_the_request_the_notes_
now=1_100.0,
)
page.encode("ascii")
- assert ""
assert data["metrics"]["training_request"]["requires"][1]["check"] == "osascript -e '1 < 2'"
@@ -525,17 +532,20 @@ def test_a_promoted_pending_step_reads_promoted_at_the_promote_step(tmp_path: Pa
dispatcher.promote(SCENARIO, pending_id)
rows = list(reversed(scenario.releases()))
assert rows[5]["operation"] == "promote" and rows[5]["rollback_target_release_id"] == pending_id
- assert rows[4]["pending"] is True and verdict_of(rows[4]) == "pending"
- assert verdict_of(rows[4], rows) == "promoted at step 5"
+ assert rows[4]["pending"] is True and result_of(rows[4]) == "pending"
+ assert result_of(rows[4], rows) == "promoted at step 5"
assert served_step(rows) == 5
page = _page(dispatcher, 4)
sub = _sub(page)
assert 'promoted at step 5' in sub and "current" not in sub
- verdict = _section(page, "Verdict")
- assert '
promoted at step 5
' in verdict
- assert "won the gate and was promoted at step 5; the release that step published serves it" in verdict
- assert "waits for a promote" not in verdict
- # The step still diffs against the head it was gated on; the promote's own page names it as the target.
+ selection_result = _section(page, "Result")
+ assert '
promoted at step 5
' in selection_result
+ assert (
+ "passed the checks and was promoted at step 5; the release that step published serves it"
+ in selection_result
+ )
+ assert "waits for a promote" not in selection_result
+ # The step still diffs against the head it was evaluated on; the promote's own page names it as the target.
assert f"+++ pi-agent/extensions/hello.ts ({pending_id[:8]})" in _section(page, "What changed")
promoted = _page(dispatcher, 5)
assert "| current" in _sub(promoted)
@@ -568,7 +578,7 @@ def test_a_step_that_published_nothing_chains_to_the_head_it_ran_on() -> None:
assert before_release_id(skipped) == "rel-1" and served_step(rows) == 1
# Chain is the last section, so cut it before the data block, which carries the row's own parent id.
chain = _section(build_release_page(2, rows), "Chain").partition("")[0]
- assert '
ran on
rel-1 (the head at this step; nothing was gated)
' in chain
+ assert '
ran on
rel-1 (the head at this step; nothing was evaluated)
' in chain
assert "none (the candidate published nothing)" in chain
assert "
parent
" not in chain and "
this release
" not in chain and "rel-0" not in chain
published = _section(build_release_page(1, rows), "Chain").partition("")[0]
@@ -602,12 +612,13 @@ def test_the_diff_colours_lines_by_position_so_a_plus_plus_line_is_an_addition()
assert '---n;' in changed and '+++n;' in changed
-def test_the_page_shows_the_proposers_design_and_review_and_escapes_them() -> None:
+@pytest.mark.parametrize("review_key", ["result", "verdict"])
+def test_the_page_shows_the_proposers_design_and_review_and_escapes_them(review_key) -> None:
creation = {"release_id": "rel-0", "parent_release_id": None, "operation": "creation"}
notes = {
"design": "Add an command.\n\nA rule alone would assume the state holds.",
"review": {
- "verdict": "partial",
+ review_key: "partial",
"covered": ["the /away command toggles the state", "the rule reads a < b"],
"uncovered": ["no notice when the state clears"],
},
@@ -621,7 +632,7 @@ def test_the_page_shows_the_proposers_design_and_review_and_escapes_them() -> No
}
page = build_release_page(1, [creation, row])
page.encode("ascii")
- assert _sections(page) == ["Why", "Design", "What changed", "Review", "Verdict", "Setup", "Chain"]
+ assert _sections(page) == ["Why", "Design", "What changed", "Review", "Result", "Setup", "Chain"]
design = _section(page, "Design")
assert '
Add an <away> command.\n\nA rule alone would assume the state holds.
' in design
review = _section(page, "Review")
@@ -638,23 +649,23 @@ def test_the_page_shows_the_proposers_design_and_review_and_escapes_them() -> No
assert "" not in page and "notice" not in page and "Xcomplete' in review
assert '
uncovered
nothing left uncovered
' in review
assert "no requires item names them" not in review
- # The review call failed: no verdict on record, but the undeclared variables still show.
+ # The review call failed: no result on record, but the undeclared variables still show.
failed = {"design": "One command.", "undeclared_env": ["TWILIO_SID"]}
page = build_release_page(1, [creation, {**row, "metrics": {**row["metrics"], "proposal_notes": failed}}])
- assert _sections(page) == ["Why", "Design", "What changed", "Review", "Verdict", "Setup", "Chain"]
+ assert _sections(page) == ["Why", "Design", "What changed", "Review", "Result", "Setup", "Chain"]
review = _section(page, "Review")
assert '
no review on record
' in review and "
covered
" not in review
assert 'TWILIO_SID' in review
-def test_the_verdict_names_why_the_proposer_produced_nothing_when_the_step_recorded_it() -> None:
+def test_the_result_names_why_the_proposer_produced_nothing_when_the_step_recorded_it() -> None:
creation = {"release_id": "rel-0", "parent_release_id": None, "operation": "creation"}
failure = "model call failed after 58.2 s (max_tokens=16384): model endpoint returned content"
row = {
@@ -669,12 +680,12 @@ def test_the_verdict_names_why_the_proposer_produced_nothing_when_the_step_recor
}
page = build_release_page(1, [creation, row])
page.encode("ascii")
- # A failure alone adds no Design or Review section; the Verdict table names it after the skip row, escaped.
- assert _sections(page) == ["Why", "What changed", "Verdict", "Setup", "Chain"]
+ # A failure alone adds no Design or Review section; the Result table names it after the skip row, escaped.
+ assert _sections(page) == ["Why", "What changed", "Result", "Setup", "Chain"]
assert (
"
skipped
no proposal
proposer failure
model call failed after "
"58.2 s (max_tokens=16384): model endpoint returned <non-text> content
"
- ) in _section(page, "Verdict")
+ ) in _section(page, "Result")
assert "" not in page.partition("" would otherwise close the data block.
+ subtitle += f"Recorded {stamp(recorded)}"
+ else:
+ subtitle += f"{escape(status_label(selection_result))} in this scenario's catalog"
+ # Every "<" leaves the JSON as \\u003c: a code text holding "" would otherwise close the data block.
data = json.dumps(row, ensure_ascii=True, sort_keys=True).replace("<", "\\u003c")
- page = (
- f"{title}\n\n\n"
- f'
\n{_chain(step, row, rows)}\n"
- "\n"
- f'\n'
+ served = served_step(rows)
+ return document(
+ title=f"Harness step {step}",
+ style=STYLE,
+ breadcrumb="Versions",
+ context=link_query.get("scenario", "") if link_query else "",
+ state=selection_result,
+ eyebrow="Harness evolution",
+ heading=f"Harness step {step}",
+ subtitle=subtitle,
+ # The served head is this scenario's home; on its own page the crumb stays text.
+ home="" if served is None or served == step else step_href(served, link_query),
+ body=_steps_nav(step, rows, link_query)
+ + '
'
@@ -279,23 +199,21 @@ def result_html(step: int, rows: Sequence[Mapping[str, object]], link_query: Map
metrics = metrics if isinstance(metrics, Mapping) else {}
selection_result = result_of(row, rows)
parts = [
- f'
{span(selection_result)}
'
- f"
{_esc(meaning(selection_result, row, metrics))}
",
+ f'
{status_span(selection_result)}
'
+ f"
{escape(meaning(selection_result, row, metrics))}
",
'
',
f"
Step
{step}
",
- f'
Release
{_esc(row.get("release_id"))}
',
+ f'
Release
{escape(row.get("release_id"))}
',
"
",
]
if metrics.get("error"):
- parts.append(f'
Error
{_esc(metrics["error"])}
')
+ parts.append(f'
Error
{escape(metrics["error"])}
')
notes = metrics.get("proposal_notes")
failure = notes.get("failure") if isinstance(notes, Mapping) else None
if isinstance(failure, str) and failure.strip():
- parts.append(f'
Proposer failure
{_esc(failure)}
')
- href = f"/reef/harness/releases/{step}/page"
- if link_query:
- # Carry the scenario and authentication to the version page without displaying the token.
- href += "?" + urlencode(dict(link_query))
+ parts.append(f'
Proposer failure
{escape(failure)}
')
+ # Carry the scenario and authentication to the version page without displaying the token.
+ href = step_href(step, link_query)
if selection_result == "pending":
command = f"/reef-versions {step} promote"
action = "Review, then promote"
@@ -311,7 +229,7 @@ def result_html(step: int, rows: Sequence[Mapping[str, object]], link_query: Map
"
Run this in your reef-pi session. You can keep chatting until you are ready.
{_esc(operation)}'
- f'{_esc(mutation.get("id") or "?")}'
- f'{_esc(kind or "?")}
'
+ f'
{escape(operation)}'
+ f'{escape(mutation.get("id") or "?")}'
+ f'{escape(kind or "?")}
'
)
return '
' + "".join(items) + "
"
@@ -342,10 +260,10 @@ def review_html(metrics: Mapping[str, object]) -> str:
return ""
uncovered = review.get("uncovered")
items = [item for item in uncovered if isinstance(item, str)] if isinstance(uncovered, Sequence) else []
- listed = "
" + "".join(f"
{_esc(item)}
" for item in items) + "
" if items else ""
+ listed = "
" + "".join(f"
{escape(item)}
" for item in items) + "
" if items else ""
return (
f'\n
Review
\n
Coverage of the request: '
- f'{span(str(review.get("result", review.get("verdict")) or "unknown"))}
\n'
+ f'{status_span(str(review.get("result", review.get("verdict")) or "unknown"))}\n'
+ (f"
Still uncovered
{listed}\n" if items else '
Nothing left uncovered.
\n')
+ "\n"
)
@@ -368,16 +286,13 @@ def build_request_page(
running step, counted only when it names this request; ``consumed`` says
whether the trainer's reserved batch carries the request. ``link_query``
is carried to the version page link. ``now`` is the clock the elapsed
- times are read against. Pure ASCII out: other characters leave as
- numeric references.
+ times are read against.
"""
record_id = str(record["agent_record_id"])
now = time.time() if now is None else now
step = settled_step(rows, record_id)
state = result_of(rows[step], rows) if step is not None else request_state(record, progress, consumed)
- title = f"Harness request {record_id[:8]}"
head = "" if step is not None else f'\n'
- state_class = state.split(" ")[0]
if step is None:
body = f'\n
Progress
\n{progress_html(record, state, progress, now)}\n'
subtitle = "Follow your request from instruction to outcome."
@@ -409,31 +324,29 @@ def build_request_page(
stage_class, marker, current = "", f"0{index + 1}", ""
journey.append(
f'
"
- '\n"
- "\n\n\n"
+ served = served_step(rows)
+ return document(
+ title=f"Harness request {record_id[:8]}",
+ style=STYLE,
+ breadcrumb="Requests",
+ context=scenario,
+ state=state,
+ eyebrow="Harness evolution",
+ heading="Request details",
+ subtitle=escape(subtitle),
+ # The served head is this scenario's home, the version this request is asked against.
+ home="" if served is None else step_href(served, link_query),
+ head=head,
+ body=(
+ '' + "".join(journey) + "\n"
+ '
\n'
+ f"
Request
\n{request_html(record)}\n"
+ f"{body}
"
+ ),
)
- return page.encode("ascii", "xmlcharrefreplace").decode("ascii")
-__all__ = ["REFRESH_SECONDS", "build_request_page", "settled_step"]
+__all__ = ["REFRESH_SECONDS", "STATE_WORDS", "build_request_page", "request_state", "settled_step"]
diff --git a/reef/service/request_service.py b/reef/service/request_service.py
index 329b93e2e..1b47b3488 100644
--- a/reef/service/request_service.py
+++ b/reef/service/request_service.py
@@ -621,7 +621,9 @@ def harness_step_records(self, headers: Mapping[str, str], step: int, relative:
except FileNotFoundError as error:
raise ArtifactNotFound("record file is not retained") from error
- def harness_release_page(self, headers: Mapping[str, str], step: int) -> str:
+ def harness_release_page(
+ self, headers: Mapping[str, str], step: int, link_query: Mapping[str, str] | None = None
+ ) -> str:
"""One HTML page for the catalog row at ``step``, counted oldest first with the creation row as 0.
The rows are the ones ``harness_releases`` answers, so the step a
@@ -629,7 +631,9 @@ def harness_release_page(self, headers: Mapping[str, str], step: int) -> str:
step ran on (the parent of a win, the head a rejected or skipped
step ran on) comes through the artifact snapshot when it is
restorable, so an extension update shows as a diff, else as its new
- text. An unknown step raises ArtifactNotFound naming the range.
+ text. ``link_query`` is carried to the Chain's links, so a page
+ opened through query parameters links pages that open the same way.
+ An unknown step raises ArtifactNotFound naming the range.
"""
scenario = self._file_scenario(headers)
rows = list(reversed(scenario.releases()))
@@ -659,6 +663,7 @@ def harness_release_page(self, headers: Mapping[str, str], step: int) -> str:
before_entries=before_entries,
before_files=before_files,
node_paths=None if descriptor is None else descriptor.node_paths,
+ link_query=link_query,
)
def harness_request_page(
diff --git a/reef/service/routes/system.py b/reef/service/routes/system.py
index 5af2daaf9..08393ab30 100644
--- a/reef/service/routes/system.py
+++ b/reef/service/routes/system.py
@@ -39,7 +39,9 @@ async def harness_releases(request: web.Request) -> web.Response:
async def harness_release_page(request: web.Request) -> web.Response:
step = int(request.match_info["step"])
headers = page_headers(request.headers, request.query)
- page = await asyncio.to_thread(request_service.harness_release_page, headers, step)
+ # The chain's links open the way this page was opened: the query parameters travel with them.
+ link_query = {key: request.query[key] for key in ("scenario", "token") if key in request.query}
+ page = await asyncio.to_thread(request_service.harness_release_page, headers, step, link_query)
return web.Response(text=page, content_type="text/html")
async def harness_request_page(request: web.Request) -> web.Response:
diff --git a/tests/reef_service/test_release_page.py b/tests/reef_service/test_release_page.py
index 226bf7c81..21111839c 100644
--- a/tests/reef_service/test_release_page.py
+++ b/tests/reef_service/test_release_page.py
@@ -12,8 +12,11 @@
from __future__ import annotations
import asyncio
+import html
import json
+import re
from pathlib import Path
+from urllib.parse import urlencode
import pytest
from aiohttp.test_utils import TestClient, TestServer
@@ -26,12 +29,14 @@
from reef.harness.episodes.run import EpisodeResult
from reef.recipe.cordis import CordisRecipe
from reef.service.app import create_app
+from reef.service.page_chrome import status_label
from reef.service.release_page import before_release_id, build_release_page, result_of, served_step
from reef.storage.sqlite import SQLiteScenarioStorage
from reef.train.cordis_backend import Mutation
from reef.train.cordis_backend.strategies import resolve_episode_scorer, resolve_proposer
MODULE = Path(__file__).parents[2] / "reef" / "service" / "release_page.py"
+CHROME = Path(__file__).parents[2] / "reef" / "service" / "page_chrome.py"
OLD_CODE = "export default function hello(pi) {\n if (1 < 2) return;\n}\n"
NEW_CODE = 'export default function hello(pi) {\n if (1 < 2) return;\n pi.on("session_start", () => {});\n}\n'
@@ -173,7 +178,7 @@ def _page(dispatcher: Dispatcher, step: int) -> str:
def _sections(page: str) -> list[str]:
- return [line[4:-5] for line in page.splitlines() if line.startswith("
")]
+ return re.findall(r"
([^<]+)
", page)
def _data(page: str) -> dict:
@@ -184,14 +189,24 @@ def _data(page: str) -> dict:
def _section(page: str, name: str) -> str:
+ """One card's body: from its heading to the close of the section it sits in."""
_, _, tail = page.partition(f"
{name}
")
- body, _, _ = tail.partition("
")
+ body, _, _ = tail.partition("")
return body
def _sub(page: str) -> str:
- """The line under the title: the release id, the result, current on the served head, the commit time."""
- return next(line for line in page.splitlines() if line.startswith('
'))
+ """The line under the title: the release id, the served marker and the commit time."""
+ _, _, tail = page.partition('
')
+ body, _, _ = tail.partition("
")
+ return body
+
+
+def _hero(page: str) -> str:
+ """The status pill beside the title, which carries the step's result."""
+ _, _, tail = page.partition('
" in page
assert _sections(page) == ["Why", "What changed", "Result", "Setup", "Chain"]
assert FIRST in _section(page, "Why") and "3f1c2a9d0b7e" in _section(page, "Why")
changed = _section(page, "What changed")
- assert 'updater1 rules' in changed
- assert "
marker rules
" in changed
+ assert 'updater1' in changed
+ assert 'rules' in changed and "
marker rules
" in changed
selection_result = _section(page, "Result")
- assert '
selected
' in selection_result
- assert "
wins
1
" in selection_result and "
losses
0
" in selection_result
+ assert 'Published' in selection_result
+ assert "
Wins
1
" in selection_result and "
Losses
0
" in selection_result
assert (
- "
candidate score
1.0
" in selection_result
- and "
current score
0.0
" in selection_result
+ "
Candidate score
1.0
" in selection_result
+ and "
Current score
0.0
" in selection_result
)
- assert "
episode failures
0
" in selection_result
+ assert "
Episode failures
0
" in selection_result
assert "nothing to set up" in _section(page, "Setup")
chain = _section(page, "Chain")
assert rows[0]["release_id"] in chain and rows[1]["release_id"] in chain
# The pending extension update was evaluated against this head, so it is this release's child.
- assert f"
step 4 {rows[4]['release_id']}" in chain
+ assert f">Step 4{rows[4]['release_id']}" in chain
data = _data(page)
assert data["release_id"] == rows[1]["release_id"] and data["metrics"]["training_request"]["text"] == FIRST
assert data["metrics"]["training_request"]["requires"] == []
@@ -234,7 +250,8 @@ def test_the_page_for_a_pending_extension_step_diffs_the_file_against_the_head(t
assert "Harness step 4" in page
assert FOURTH.replace("<", "<") in _section(page, "Why")
changed = _section(page, "What changed")
- assert 'updateext code_extension' in changed
+ assert 'updateext' in changed
+ assert 'code_extension' in changed
# A unified diff of the rendered file against the head's copy, the added line marked.
assert f"--- pi-agent/extensions/hello.ts ({rows[1]['release_id'][:8]})" in changed
assert f"+++ pi-agent/extensions/hello.ts ({rows[4]['release_id'][:8]})" in changed
@@ -243,9 +260,10 @@ def test_the_page_for_a_pending_extension_step_diffs_the_file_against_the_head(t
# The rules bump that made the composite win rides beside it.
assert "
marker marker rules
" in changed
selection_result = _section(page, "Result")
- assert '
pending
' in selection_result and "waits for a promote" in selection_result
+ assert 'Ready for review' in selection_result
+ assert "waits for a promote" in selection_result
chain = _section(page, "Chain")
- assert f'
parent
{rows[1]["release_id"]}
' in chain
+ assert f'
Parent
{rows[1]["release_id"]}
' in chain
assert rows[4]["release_id"] in chain and "
" not in chain
data = _data(page)
assert data["pending"] is True and data["metrics"]["mutations"][1]["options"]["config"]["code"] == NEW_CODE
@@ -261,8 +279,9 @@ def test_the_page_for_a_rejected_step_names_the_head_it_ran_on_and_the_candidate
assert SECOND in _section(page, "Why") and "s2" in _section(page, "Why")
assert "
' in selection_result and "the head stayed" in selection_result
- assert "
wins
0
" in selection_result and "
losses
1
" in selection_result
+ assert 'Not selected' in selection_result
+ assert "Did not pass the checks" in selection_result and "The head stayed" in selection_result
+ assert "
Wins
0
" in selection_result and "
Losses
1
" in selection_result
chain = _section(page, "Chain")
assert rows[2]["release_id"] == rows[1]["release_id"]
assert f'{rows[1]["release_id"]} (the head at this step; the candidate published nothing)' in chain
@@ -275,11 +294,12 @@ def test_the_page_for_an_automatic_step_without_a_request_says_a_failure_in_the_
dispatcher = _chain(tmp_path)
try:
page = _page(dispatcher, 3)
- assert "
a failure in the batch
" in _section(page, "Why")
+ assert "A failure in the batch" in _section(page, "Why")
changed = _section(page, "What changed")
- assert 'creates1 skill' in changed
+ assert 'creates1' in changed
+ assert 'skill' in changed
assert "
# notes
" in changed and "notes" in changed
- assert '
rejected
' in _section(page, "Result")
+ assert 'Not selected' in _section(page, "Result")
# The trainer writes training_request on every request step's row; an automatic step has none.
assert "training_request" not in _data(page)["metrics"]
finally:
@@ -294,26 +314,127 @@ def test_the_chain_lists_the_steps_evaluated_against_a_release_as_its_children(t
assert "Harness step 0" in page
assert "no step made it" in _section(page, "Why")
assert "the seed as the recipe rendered it" in _section(page, "What changed")
- assert '
creation
' in _section(page, "Result")
+ assert 'Starting point' in _section(page, "Result")
chain = _section(page, "Chain")
# Only the win ran on the seed; the two rejected candidates ran on the win, whose id their rows carry.
- assert chain.count("
step 1 {rows[1]["release_id"]}selected'
+ f'>Step 1{rows[1]["release_id"]}Published'
in chain
)
assert '' not in chain
+ # Each child links the page of its own step, opened the way this one was.
+ assert 'href="/reef/harness/releases/1/page"' in chain
head = _section(_page(dispatcher, 1), "Chain")
- assert head.count("
step ") == 3 and "
step 1 " not in head
+ assert head.count(">Step ") == 3 and ">Step 1" not in head
for step in (2, 3):
- assert f'
step {step} {rows[1]["release_id"]}' in head
+ assert f'>Step {step}{rows[1]["release_id"]}step 4 {rows[4]["release_id"]}pending' in head
+ f'>Step 4{rows[4]["release_id"]}Ready for review'
+ in head
)
finally:
dispatcher.close()
+def test_the_page_carries_the_shared_chrome_and_links_the_other_steps_with_the_query_it_was_opened_with() -> None:
+ """The version page and the request page are one design: the same logo, header and status words.
+
+ A person opens either from a browser, which sends no header, so the
+ scenario and the token ride in the query; every link the page draws has
+ to carry them on or the next page is a 401."""
+ creation = {"release_id": "rel-0", "parent_release_id": None, "operation": "creation"}
+ row = {
+ "release_id": "rel-1",
+ "parent_release_id": "rel-0",
+ "operation": "training",
+ "metrics": {"selected": True},
+ }
+ query = {"scenario": "a b", "token": "t&<"}
+ page = build_release_page(0, [creation, row], link_query=query)
+ page.encode("ascii")
+ logo = (MODULE.parents[2] / "docs" / "assets" / "reef-logo-light.svg").read_text().strip()
+ # The same logo the request page carries, which its own test pins against the same file.
+ assert logo in page
+ assert page.startswith("") and '' in page
+ assert '' in page
+ assert '' in page
+ # The scenario names the page in the header, and the token never shows.
+ assert 'a b' in page and "t&<" not in page
+ assert "Versions" in page
+ assert 'href="/reef/harness/releases/1/page?scenario=a+b&token=t%26%3C"' in _section(page, "Chain")
+ # Without a query the links stay bare, as a curl with headers reads them.
+ assert 'href="/reef/harness/releases/1/page"' in _section(build_release_page(0, [creation, row]), "Chain")
+
+
+def test_the_top_bar_and_the_step_walk_lead_to_the_pages_a_browser_can_open() -> None:
+ """Everything that looks clickable is: the logo and the crumb go to the served head, the arrows to the neighbours.
+
+ Only the two ``/page`` routes take the token from the query, so those are
+ the only places a link can lead; the catalog itself answers JSON and
+ refuses a query token."""
+ rows = [
+ {"release_id": "rel-0", "parent_release_id": None, "operation": "creation"},
+ {"release_id": "rel-1", "parent_release_id": "rel-0", "operation": "training", "metrics": {"selected": True}},
+ {"release_id": "rel-1", "parent_release_id": "rel-0", "operation": "training", "metrics": {"selected": False}},
+ ]
+ query = {"scenario": "agents", "token": "secret"}
+ head = f"/reef/harness/releases/1/page?{urlencode(query)}"
+ assert served_step(rows) == 1
+
+ middle = build_release_page(2, rows, link_query=query)
+ # The logo and the Harness crumb lead to the served head.
+ assert f'' in middle
+ assert f'Harness' in middle
+ walk = middle.partition('