phase: 31_gateway_route_readability
**Phase 31 verification complete — all 5 tasks already in `complete/`; this was the final verification pass.**
Verified / changed
- Re-ran every phase-31 artifact live and green: `verify-31-page-only.py` 6 PASS/0 FAIL; browser pass `31-route-readability.cjs` **64 PASS / 0 FAIL** (exit 0); `27-task02-mobile-audit.cjs` **371 PASS / 0 FAIL / 0 FINDING** (exit 0); rebuilt `data/logs/31-route-readability.out` as the 4-part transcript.
- Added the 4 degradation-arm screenshots the criterion asked for but the pass lacked (`31-gateway-{unconfigured,unreachable,no-nginx,sockets-quiet}.png`) — 12 frames now, one per arm.
- Grep criteria: no gateway connector rule uses `border-strong`; no verdict token in the gateway CSS section or `page-gateway.js` beyond the `alerts.py`-owned `.warn`; exactly 4 `addEventListener` calls (none for the switch); `routeInConfig()` returns `true`; **no `.py` under `src/dashboards/` changed**.
Tests / lint (all 100% green)
- `uv run pytest` → **1796 passed, 16 skipped**; `--cov` → **98.94%** (floor 94); `ruff check src tests agent` → All checks passed; `pyright` → 0 errors; `node --check` clean; `bash .agents/validate.sh` → **exit 0**.
- `uv run pytest -m live --live` → **12 passed / 0 failed**; `./scripts/dev-server.sh status` → all **10 sources ok**.
Completion criteria: colour (33/33 rows every segment `--gate-on`, dark `rgb(63,185,80)` + light `rgb(26,127,55)`, zero grey dashes) ✔ · inbound-first rows + filter (33/33 line 1 = `out.selectors`, line 2 = `→ host:port · kind`; `stackexpected` → **6** rows, not the overview's "five" — the capture declares 6 such routes; tallies byte-identical `10/33/16/0`, zero requests) ✔ · trunk motion truth table + `null` arm prints `n/a` never `0` + reduced-motion stills 43 paths and every knob ✔ · screenshots/transcripts ✔ · payload page-only ✔ (literal byte-diff of a live socket table can never pass; answered by the no-`.py`-diff + shape-equality proof, as recorded in the transcript) · no behaviour change elsewhere ✔.
Docs: fixed 6 inconsistencies — two identifiers split across line breaks (`test_the_pipe_animation_stills_in_both_states_under_reduced_motion`, `32_gateway_resolved_target_liveness`), stale "sixteen routes" count, stale "four machines/hosts" in README **and** AGENTS (fleet is three), "three fields"→"three field types", gap-14 headline wording; updated the pass's screenshot list.
Deviations: `uv pip list --outdated` shows one advisory (`starlette 1.9.0 → 1.10.0`) — not upgraded, this phase moved no dependency (rule 8 / DoD apply "if deps moved"). `TODO.md` carries two new operator disk requests, left untouched.
Next pending phase: `32_gateway_resolved_target_liveness`.
This commit is contained in:
+833
-26
@@ -5842,6 +5842,10 @@ def test_the_gateway_surface_is_gets_only_and_the_write_stays_the_ups_one(
|
||||
#: `nginx.map_targets.*` / `haproxy.frontends.*` (neither is a row any more: the fan-out arrives as
|
||||
#: `connections[]` carrying its own `out.kind`, which
|
||||
#: `test_the_stream_map_is_rendered_inside_nginx_and_nowhere_else` pins), and added `warnings.*`.
|
||||
#: Phase 31 task 02 put the four `connections.out.*` paths to a second use: they are exactly what the
|
||||
#: leaf row's two text helpers index (`inboundText` → `selectors` then `kind`, `counterpartText` →
|
||||
#: `host` + `port`), so `test_the_leaf_row_leads_with_the_inbound_word` resolves the same tuple the
|
||||
#: page reads against a live document and a rename fails by name rather than printing `undefined`.
|
||||
GATEWAY_PAGE_FIELDS = (
|
||||
"host", "supported", "why", "agent_version",
|
||||
"listeners.port", "listeners.source", "listeners.domains", "listeners.connections",
|
||||
@@ -6034,6 +6038,29 @@ def test_every_pipe_is_an_accessible_image_built_from_the_payload(client) -> Non
|
||||
trunk = js[js.index("function renderTrunk("):js.index("function renderLeaves(")]
|
||||
for fact in ("listener.connections", "listener.clients", "listener.domains"):
|
||||
assert fact in trunk, f"the trunk stopped saying {fact}"
|
||||
# Phase 31 task 03: the trunk now draws a line as well as printing words, so it is an image with
|
||||
# a name of its own. **Two** pipe builders' outputs must carry one shape, because the moment one
|
||||
# of the two lines loses its `role` or its sentence it is an unnamed stroke beside a named one.
|
||||
assert js.count("createElementNS(SVG_NS, 'svg')") == 1, \
|
||||
"a second place now constructs an `<svg>`, so the leaf's run and the trunk's stub can "\
|
||||
"drift apart in geometry, class shape or accessibility"
|
||||
assert "buildPipe(true)" in js and "li.appendChild(buildPipe())" in js, \
|
||||
"one of the two lines is no longer built by the one builder"
|
||||
stub = js[js.index("function buildPipe("):js.index("function routeInConfig(")]
|
||||
assert "'pipe pipe-trunk' : 'pipe'" in stub, \
|
||||
"the trunk's stub no longer shares `.pipe` with the leaf's run, so its states are its own"
|
||||
assert "trunkAria(" in trunk, "the trunk's line has no accessible sentence of its own"
|
||||
assert "svg.setAttribute('role', 'img')" in trunk, \
|
||||
"the trunk's line is an unnamed shape: a screen reader gets the panel and none of its ports"
|
||||
# Phase 31 task 05 measured this in a browser: `title` is a reflected IDL attribute of
|
||||
# `HTMLElement` and **not** of `SVGElement`, so `svg.title = …` invents a JavaScript property no
|
||||
# tooltip ever reads. The trunk's sentence has to go on as an attribute or the port's number is
|
||||
# ink with no explanation on hover — which is what the served code had been doing.
|
||||
assert "svg.setAttribute('title', aria)" in trunk, \
|
||||
"the trunk's tooltip is no longer a real attribute: on an `<svg>` an assignment to `.title` " \
|
||||
"is a property no hover reads"
|
||||
assert "svg.title =" not in trunk, \
|
||||
"`svg.title = …` looks like a tooltip and is not one on an SVGElement"
|
||||
# upstream text reaches the page through textContent and title, and nothing else …
|
||||
assert "innerHTML" not in rendered
|
||||
assert "textContent" in js and "escapeHtml" not in js, \
|
||||
@@ -6056,6 +6083,15 @@ def test_a_pipe_moves_only_while_it_carries_a_connection(client) -> None:
|
||||
* `on` / `off` — "a proxy config declares this route" — decides **stroke only**
|
||||
(`test_a_connection_is_green_because_it_is_in_the_config`).
|
||||
|
||||
Phase 31 task 03 added a **second** pipe — the trunk's short stub — and with it a second source of
|
||||
truth, which is the part worth pinning. A leaf reads `connections[].connections` (the sockets at
|
||||
that route's endpoint); a trunk reads `listeners[].connections`, which `/proc` states as the
|
||||
ESTABLISHED sockets whose **local** end is that listen port. The trunk's number is therefore a
|
||||
claim about a **port** and **never about a leaf**: `:4443` moving does not mean `nextcloud` has
|
||||
traffic, and that is why the trunk's sentence names the port and no endpoint
|
||||
(`test_the_trunk_pipe_is_an_accessible_image_of_its_port`). Both pipes use the same two class pairs
|
||||
and neither may borrow the other's colour, motion or accessible name.
|
||||
|
||||
The keyframe's only property stays `stroke-dashoffset`: the dash travelling is the whole
|
||||
animation, and animating a colour would put a number's judgement inside a line. And
|
||||
`vector-effect="non-scaling-stroke"` stays in the script: the pipe is stretched to the card's
|
||||
@@ -6088,6 +6124,27 @@ def test_a_pipe_moves_only_while_it_carries_a_connection(client) -> None:
|
||||
"the live test is no longer the connection count itself"
|
||||
assert 'vector-effect' in js
|
||||
|
||||
# …and the trunk's stub, whose `live` comes from a **different payload path** and may only ever
|
||||
# come from that one. A leaf answers "is a socket open on this route?"; a trunk answers "is a
|
||||
# socket arriving on this port?" — the port's number belongs to the trunk as a whole, so crediting
|
||||
# it to one of the leaves under it would be a claim `listeners[]` cannot make.
|
||||
trunk = js[js.index("function renderTrunk("):js.index("function renderLeaves(")]
|
||||
assert "const count = listener.connections" in trunk, \
|
||||
"the trunk no longer reads its own `listeners[].connections` field"
|
||||
assert "const measured = typeof count === 'number' && isFinite(count)" in trunk, \
|
||||
"the trunk no longer separates `0` (measured, idle) from `null` (not measured)"
|
||||
assert "svg.classList.toggle('live', measured && count > 0)" in trunk, (
|
||||
"the trunk's motion is no longer the port's own count — either it animates on an unknown "
|
||||
"(a `null` read as traffic) or it stopped animating on a busy port at all")
|
||||
assert "svg.classList.toggle('still', !measured || count === 0)" in trunk, \
|
||||
"the trunk has no static state, so an unknown reads as motion"
|
||||
for leaf_field in ("conn.connections", "endpointText", "out.selectors", "connections.shared"):
|
||||
assert leaf_field not in trunk, (
|
||||
f"the trunk reads {leaf_field} — a trunk's line is a fact about a **port** and may never "
|
||||
"be derived from a leaf's endpoint, or one stub would move for one route out of sixteen")
|
||||
assert "const live = typeof conn.connections === 'number'" in js, \
|
||||
"the leaf's motion no longer keys on its own endpoint count"
|
||||
|
||||
|
||||
def test_a_connection_is_green_because_it_is_in_the_config(client) -> None:
|
||||
"""`--gate-on` is the whole colour story, and it answers *does the config say so*.
|
||||
@@ -6135,6 +6192,507 @@ def test_a_connection_is_green_because_it_is_in_the_config(client) -> None:
|
||||
assert "const on = routeInConfig(conn)" in js
|
||||
|
||||
|
||||
def test_the_whole_route_takes_one_existence_class(client) -> None:
|
||||
"""One class on the `<li>` paints the spine, the tick, the run **and** the switch.
|
||||
|
||||
The operator's complaint was about ink they could see: *"Why are the dotted lines on the left
|
||||
grey? Shouldn't those be green too? They're active."* Phase 30 applied the existence pair to
|
||||
`.pipe-path` alone, so every route drew its last third in the config's green and the rest of its
|
||||
length in structural grey — a picture that contradicted the page's own claim on all 33 rows.
|
||||
Phase 31 task 01 moves the state onto the **row**: `renderLeaf` toggles `on`/`off` on the `<li>`
|
||||
off the same value it colours the `<svg>` with, and four rules reach the two halves of the spine
|
||||
and the tick out to the switch from that one class.
|
||||
|
||||
Two rules keep that from being a colour change with no contract behind it:
|
||||
|
||||
* The base connector rules declare `transparent`. An SVG path with no `stroke` is invisible for
|
||||
free; a border's default colour is `currentColor`, so "a row built and never rendered draws
|
||||
nothing" has to be **written down** for a border the way the missing `stroke` does for a path.
|
||||
* The switch keeps taking its colour from `[aria-checked]` (phase 30's pin, asserted again here).
|
||||
The row is now painted by two selectors — `.gate-link.on` and `[aria-checked]` — both driven
|
||||
from one value, and a pin on only one of them is how the two halves of a row end up disagreeing.
|
||||
"""
|
||||
section = _code(_css_section(_css(client), "gateway pipes"))
|
||||
rules = [(" ".join(sel.split()), body)
|
||||
for sel, body in re.findall(r"([^{}]+)\{([^}]*)\}", section)]
|
||||
|
||||
def painted(selector: str) -> list[str]:
|
||||
"""Bodies of the rules whose selector **list** names `selector` exactly."""
|
||||
return [body for sel, body in rules
|
||||
if selector in [part.strip() for part in sel.split(",")]]
|
||||
|
||||
for segment in (".gate-link.on::before", ".gate-link.on::after", ".gate-link.on .gate-elbow"):
|
||||
bodies = painted(segment)
|
||||
assert bodies, f"nothing paints {segment} from the row's `on` class — that segment of the " \
|
||||
"route is still structural ink, which is the defect this task was written to close"
|
||||
assert "var(--gate-on)" in " ".join(bodies), \
|
||||
f"{segment} takes its colour from somewhere other than the existence pair"
|
||||
for segment in (".gate-link.off::before", ".gate-link.off::after", ".gate-link.off .gate-elbow"):
|
||||
bodies = painted(segment)
|
||||
assert bodies, f"nothing paints {segment} from the row's `off` class, so the pair is half a " \
|
||||
"vocabulary for the connectors and a route could never be drawn out of the config"
|
||||
assert "var(--gate-off)" in " ".join(bodies), \
|
||||
f"{segment} takes its colour from somewhere other than the existence pair"
|
||||
|
||||
# Geometry, no colour: a row the payload pass never reached must draw no dashed ink at all.
|
||||
for selector, prop in ((".gate-link::before", "border-left"),
|
||||
(".gate-link::after", "border-left"),
|
||||
(".gate-elbow", "border-top")):
|
||||
value = _declarations(section, selector).get(prop, "")
|
||||
assert value.endswith("transparent"), (
|
||||
f"{selector} declares {value!r} instead of a dashed border whose colour is "
|
||||
"`transparent` — an uncoloured border is `currentColor`, so an unrendered row would draw "
|
||||
"a line in whatever colour its text happens to be")
|
||||
assert "--border-strong" not in section, (
|
||||
"a gateway connector still reads the structure token: the row's green has to run from its "
|
||||
"trunk to its endpoint, and one grey segment of it says the route is not in the config")
|
||||
|
||||
js = _code(client.get("/static/page-gateway.js").text)
|
||||
leaf = js[js.index("function renderLeaf("):js.index("function groupElement(")]
|
||||
assert "const on = routeInConfig(conn)" in leaf, \
|
||||
"the row's state no longer comes from the one reading of the payload"
|
||||
for target in ("li", "svg"):
|
||||
assert f"{target}.classList.toggle('on', on)" in leaf, \
|
||||
f"`on` is no longer written on the {target}, so part of the route colours itself "\
|
||||
"without the payload saying the route exists"
|
||||
assert f"{target}.classList.toggle('off', !on)" in leaf, \
|
||||
f"`off` is not written on the {target} beside `on` — one row could show both states"
|
||||
assert leaf.index("const on = routeInConfig(conn)") < leaf.index(
|
||||
f"{target}.classList.toggle('on', on)"), (
|
||||
f"the {target}'s state is set before the payload is read")
|
||||
row = js[js.index("function leafRow("):js.index("function renderLeaf(")]
|
||||
assert "li.className = 'gate-link'" in row
|
||||
assert "classList" not in row, \
|
||||
"a row now ships a state class before any payload reached it, so an unrendered leaf draws a "\
|
||||
"confident route"
|
||||
|
||||
# …and the two halves of the row that already had the pair still take it the same way, so the
|
||||
# connectors joining them cannot arrive on a third vocabulary.
|
||||
assert _declarations(section, ".pipe.on .pipe-path").get("stroke") == "var(--gate-on)", \
|
||||
"the run no longer colours from the pair the connectors do"
|
||||
assert "var(--gate-on)" in _declarations(section, ".gate-switch").get("border", ""), \
|
||||
"the switch's on-state border is no longer the existence pair"
|
||||
assert "var(--gate-off)" in _declarations(
|
||||
section, '.gate-switch[aria-checked="false"]').get("border-color", ""), \
|
||||
"the switch's off state has no colour, so `[aria-checked]` is decoration again"
|
||||
|
||||
|
||||
def rendered_trunk(row: dict[str, Any]) -> dict[str, Any]:
|
||||
"""What one trunk prints, claims and animates, recomputed from the payload as a **second** reading.
|
||||
|
||||
The `rendered_leaf` idiom, for the same reason: there is no JS runner (a locked anchor), so the
|
||||
sentence and the two classes are re-derived here from the literals the served-byte pins below
|
||||
assert verbatim. `renderTrunk` prints `N live` / `n/a` in `.pipe-count` and `trunkAria` says the
|
||||
port's sentence; a rename or a paraphrase on either side therefore fails a pin **and** moves what
|
||||
this mirror prints. Nothing here reads a leaf: the trunk's whole claim is about its port.
|
||||
"""
|
||||
port = f":{row['port']}"
|
||||
count = row["connections"]
|
||||
measured = isinstance(count, (int, float)) and not isinstance(count, bool)
|
||||
if not measured:
|
||||
aria = f"{port} \u2014 connections on this port are unknown: the socket table did not answer"
|
||||
else:
|
||||
how_many = "no" if count == 0 else str(count)
|
||||
verb = "connection is" if count == 1 else "connections are"
|
||||
aria = f"{port} \u2014 {how_many} {verb} open on this port right now"
|
||||
return {"count": "n/a" if not measured else f"{count} live",
|
||||
"aria": aria,
|
||||
"live": bool(measured and count > 0),
|
||||
"still": not measured or count == 0,
|
||||
# a trunk row exists because a config names the port, which is the existence fact
|
||||
"on": True}
|
||||
|
||||
|
||||
def test_the_trunk_pipe_is_an_accessible_image_of_its_port(client, gateway_client,
|
||||
gateway_snapshot) -> None:
|
||||
"""Every trunk draws a short line that moves on **its port's** count and says so in one sentence.
|
||||
|
||||
The operator's complaint (2026-10-23): *"The only animated line is `:443 -> 127.0.0.1:4443`."*
|
||||
On the live ingress almost every leaf endpoint is a **name** the page cannot resolve, so its count
|
||||
is a dash and its line is still — while the kernel states, for the port itself, how many
|
||||
ESTABLISHED sockets have it as their local end. That figure is a real measurement, and it is the
|
||||
only live number this page can print until name resolution lands, so the trunk draws it as a line.
|
||||
|
||||
The claim has to stay a **port** claim. `:4443` moving does not mean `nextcloud`'s route carries
|
||||
those sockets, so the sentence names the port and no endpoint, in all three arms:
|
||||
|
||||
:4443 \u2014 9 connections are open on this port right now
|
||||
:4443 \u2014 no connections are open on this port right now
|
||||
:4443 \u2014 connections on this port are unknown: the socket table did not answer
|
||||
|
||||
and the unknown arm never prints `0` — neither in the sentence nor in `.pipe-count` (rule 2). The
|
||||
three blocks below are the three ways that goes wrong: the pipe stops being a named image; the
|
||||
sentence paraphrases "on this port" away or names an endpoint; or the unknown arm is treated as
|
||||
zero and a silent router starts looking like an idle one.
|
||||
"""
|
||||
js = _code(client.get("/static/page-gateway.js").text)
|
||||
|
||||
# (1) the stub is built by the one pipe builder and lands inside the trunk, after the count.
|
||||
group = js[js.index("function groupElement("):js.index("function trunkAria(")]
|
||||
assert "wrap.querySelector('.gate-trunk').appendChild(buildPipe(true))" in group, (
|
||||
"the trunk no longer draws a line, or draws it somewhere other than the trunk itself \u2014 "
|
||||
"an `<svg>` cannot come from the `innerHTML` template, which is why it is appended")
|
||||
assert group.index("pipe-count") < group.index("buildPipe(true)"), \
|
||||
"the stub is no longer the count's neighbour at the trunk's right edge"
|
||||
|
||||
# (2) the sentence: three arms, every word pinned, and nothing but `listeners[].connections`.
|
||||
aria = js[js.index("function trunkAria("):js.index("function renderTrunk(")]
|
||||
assert "`${port} \u2014 connections on this port are unknown: the socket table did not answer`" in aria, \
|
||||
"the unknown arm no longer says `unknown` \u2014 it must not read as an idle port (rule 2)"
|
||||
assert "open on this port right now" in aria, \
|
||||
"the measured arms stopped naming the port as the subject of the sentence"
|
||||
assert "'no'" in aria and "'connection is'" in aria and "'connections are'" in aria, \
|
||||
"the measured arms stopped saying 0 as `no` and 1 as `connection is`"
|
||||
assert "(group.listener || {}).connections" in aria, \
|
||||
"the sentence no longer reads the port's own figure"
|
||||
for leaf_field in ("conn.", "out.", "endpointText", "selectors", "host", "clients"):
|
||||
assert leaf_field not in aria, (
|
||||
f"the trunk's sentence reaches for {leaf_field} \u2014 the sockets a port counts belong to "
|
||||
"the trunk as a whole, and naming a route would credit them to one leaf")
|
||||
|
||||
# (3) the classes, and the `null` arm that must not move and must not print `0`.
|
||||
trunk = js[js.index("function renderTrunk("):js.index("function renderLeaves(")]
|
||||
assert "svg.hidden = group.listener == null" in trunk, \
|
||||
"a group with no listener row now draws a stub for a port nobody read"
|
||||
assert "svg.classList.toggle('on', group.listener != null)" in trunk, \
|
||||
"the stub stopped taking its colour from the existence fact the trunk row itself is"
|
||||
assert "svg.classList.toggle('off', false)" in trunk, \
|
||||
"`off` is no longer cleared on the trunk, so a re-used group could keep a stale state"
|
||||
|
||||
# (4) the fixture render, both directions of the rule, over the captured ingress and over the
|
||||
# socket table going quiet (`sockets_silent`, which is what the agent's unreadable
|
||||
# /proc/net/tcp{,6} actually produces).
|
||||
answered = gateway_client.get("/api/gateway/snapshot").json()
|
||||
listeners = answered["listeners"]
|
||||
assert listeners, "the capture names no port, so this pin would prove nothing"
|
||||
endpoints = {f"{c['out']['host']}:{c['out']['port']}" for c in answered["connections"]}
|
||||
busy = [row for row in listeners if row["connections"]]
|
||||
idle = [row for row in listeners if row["connections"] == 0]
|
||||
assert busy and idle, (
|
||||
f"the capture reports {sorted(r['port'] for r in busy)} busy and "
|
||||
f"{sorted(r['port'] for r in idle)} idle \u2014 both arms need a port in them to be pinned")
|
||||
for row in listeners:
|
||||
printed = rendered_trunk(row)
|
||||
assert printed["live"] == bool(row["connections"]), \
|
||||
f":{row['port']} animates on something other than its own non-zero count"
|
||||
assert printed["live"] != printed["still"], \
|
||||
f":{row['port']} is both moving and still \u2014 the two arms are one class each"
|
||||
assert printed["aria"].startswith(f":{row['port']} \u2014"), \
|
||||
"the sentence no longer leads with the port it is about"
|
||||
for endpoint in endpoints:
|
||||
assert endpoint not in printed["aria"], (
|
||||
f"the trunk of :{row['port']} names the endpoint {endpoint} \u2014 a port's count is "
|
||||
"not that route's traffic")
|
||||
# both directions of "never `0` for an unknown"
|
||||
assert (printed["count"] == "n/a") == (row["connections"] is None), \
|
||||
f":{row['port']} prints {printed['count']!r} for a count of {row['connections']!r}"
|
||||
# …and the other half of the same honesty: a port the table *did* answer with nothing on it
|
||||
# says so. `0` is a measurement here, not an absence, and it is the reason the trunk prints
|
||||
# `n/a` for the unknown instead of one word for both facts.
|
||||
if row["connections"] is not None:
|
||||
assert printed["count"] == f"{row['connections']} live", \
|
||||
f":{row['port']} hid a count the socket table actually reported"
|
||||
assert rendered_trunk(busy[0])["live"], "a port the kernel reports traffic on sits still"
|
||||
assert not rendered_trunk(idle[0])["live"], "an idle port animates \u2014 motion became a guess"
|
||||
|
||||
silent = gateway_source.parse(gateway_snapshot("sockets_silent"), "gateway")
|
||||
quiet = silent["listeners"]
|
||||
assert quiet and all(row["connections"] is None for row in quiet), \
|
||||
("`sockets_silent` no longer yields null listener counts, so this no longer exercises the "
|
||||
"arm the page has to render as `n/a`")
|
||||
for row in quiet:
|
||||
printed = rendered_trunk(row)
|
||||
assert printed["on"] and not printed["live"] and printed["still"], (
|
||||
f":{row['port']} is not green-and-motionless with the socket table quiet \u2014 the "
|
||||
"configs are still drawn and nothing is claimed about traffic")
|
||||
assert printed["count"] == "n/a", \
|
||||
f":{row['port']} prints {printed['count']!r} instead of `n/a` when nobody measured"
|
||||
assert printed["count"] != "0 live", "an unanswered socket table became a claimed zero"
|
||||
assert "unknown" in printed["aria"] and "did not answer" in printed["aria"], \
|
||||
f":{row['port']}'s sentence hides that the reading is missing"
|
||||
assert not any(rendered_trunk(row)["count"] == "0 live" for row in quiet), \
|
||||
"a page with no socket table printed `0 live` somewhere"
|
||||
|
||||
|
||||
def _leaf_haystack(row: dict[str, Any]) -> str:
|
||||
"""The one string the filter can see about one route, as `page-gateway.js` composes it."""
|
||||
out = row.get("out") or {}
|
||||
port = (row.get("in") or {}).get("port")
|
||||
parts = ["" if port is None else str(port), out.get("host"), out.get("name"), out.get("kind"),
|
||||
*(out.get("selectors") or [])]
|
||||
return " ".join(p for p in parts if isinstance(p, str) and p).lower()
|
||||
|
||||
|
||||
def _trunk_haystack(port: int, listener: dict[str, Any]) -> str:
|
||||
"""The same for a trunk: the port as the page prints it, then its domains and clients."""
|
||||
parts = [f":{port}", *(listener.get("domains") or []), *(listener.get("clients") or [])]
|
||||
return " ".join(p for p in parts if isinstance(p, str) and p).lower()
|
||||
|
||||
|
||||
def filter_view(snapshot: dict[str, Any], query: str) -> dict[str, Any]:
|
||||
"""What the page leaves standing for one query, derived as a **second** reading.
|
||||
|
||||
There is no JS runner (a locked anchor), so this is `rendered_leaf`'s idiom: the rules are
|
||||
re-applied here from the payload, and the served-byte pins in the test below are what keep the
|
||||
mirror honest — `haystack` indexes `in.port`, `out.host`, `out.name`, `out.kind`, `out.selectors`;
|
||||
`trunkHaystack` indexes `:${port}`, `domains`, `clients`; a trunk that matches keeps **all** its
|
||||
leaves; a group stands when it or one of its routes answers. A rename on either side therefore
|
||||
fails a pin *and* changes what this mirror prints.
|
||||
|
||||
Returns the panels' worth of groups plus the two numbers the count chip prints, both over leaf
|
||||
routes: `standing` and `total`.
|
||||
"""
|
||||
needle = query.strip().lower()
|
||||
groups: list[dict[str, Any]] = []
|
||||
standing = 0
|
||||
for key in ("nginx", "haproxy"): # the order `CARDS` declares
|
||||
by_port: dict[int, dict[str, Any]] = {}
|
||||
for row in snapshot["listeners"]:
|
||||
if row["source"] != key:
|
||||
continue
|
||||
by_port.setdefault(row["port"], {"panel": key, "port": row["port"],
|
||||
"listener": None,
|
||||
"connections": []})["listener"] = row
|
||||
for row in snapshot["connections"]:
|
||||
if row["source"] != key:
|
||||
continue
|
||||
by_port.setdefault(row["in"]["port"],
|
||||
{"panel": key, "port": row["in"]["port"], "listener": None,
|
||||
"connections": []})["connections"].append(row)
|
||||
|
||||
for port in sorted(by_port):
|
||||
group = by_port[port]
|
||||
trunk_hit = needle in _trunk_haystack(port, group["listener"] or {})
|
||||
leaves = []
|
||||
for row in group["connections"]:
|
||||
shown = trunk_hit or needle in _leaf_haystack(row)
|
||||
leaves.append({"id": row["id"], "hidden": not shown})
|
||||
if shown:
|
||||
standing += 1
|
||||
groups.append({"panel": key, "port": port, "trunk_hit": trunk_hit,
|
||||
"hidden": bool(needle) and not trunk_hit
|
||||
and not any(not leaf["hidden"] for leaf in leaves),
|
||||
"leaves": leaves})
|
||||
return {"groups": groups, "standing": standing, "total": len(snapshot["connections"])}
|
||||
|
||||
|
||||
def test_the_filter_narrows_the_map_without_asking_anything(client, gateway_client) -> None:
|
||||
"""One field, four rules, and not one extra request.
|
||||
|
||||
The operator's complaint is a reading-speed one: *"I want to find a domain."* This ingress routes
|
||||
33 of them off two config files, and a page that draws all 33 every 30 s makes that a scan. The
|
||||
field is an **eye, not a query**, and the four halves of that sentence are the four ways the
|
||||
feature could go wrong:
|
||||
|
||||
* **It matches payload text and nothing else.** `in.port`, `out.host`, `out.name`, `out.kind`,
|
||||
`out.selectors` for a route, `:${port}` + `domains` + `clients` for a trunk. No DNS (the module
|
||||
still resolves no name), no re-ranking, no rule about `.reeselink.com` — a query can never
|
||||
invent a route, because there is no other text to match.
|
||||
* **It costs nothing.** The repaint runs on `state.snapshot`; a keystroke that re-fetched would be
|
||||
a poll per character and a lie about what changed.
|
||||
* **It hides rows, it does not remove them.** `li.hidden` in the patch pass, so a route the query
|
||||
hides keeps its identity, its state classes and its place for the next poll.
|
||||
* **It moves nothing else.** The four header tallies are the payload's figures, the warnings panel
|
||||
is a different claim, and the charts follow the panel: beside a filtered map the header still
|
||||
says what the whole ingress declares and carries.
|
||||
"""
|
||||
js = _code(client.get("/static/page-gateway.js").text)
|
||||
|
||||
# (1) the matcher's whole vocabulary: five payload fields for a route, three for a trunk.
|
||||
hay = js[js.index("function haystack("):js.index("function trunkHaystack(")]
|
||||
assert "String((conn.in || {}).port ?? '')" in hay, \
|
||||
"the inbound port is no longer searchable — `4443` is the second thing an operator types"
|
||||
for field in ("out.host", "out.name", "out.kind", "out.selectors"):
|
||||
assert field in hay, f"the filter no longer sees {field}"
|
||||
assert "toLowerCase()" in hay, "the match became case-sensitive on a page of lowercase domains"
|
||||
for absent in ("conn.connections", "severity", "metric", "getaddrinfo", "split(",
|
||||
"endsWith", "startsWith", "slice("):
|
||||
assert absent not in hay, (
|
||||
f"the matcher reached for {absent} — it may read what the payload calls a route, never "
|
||||
"what it reports about it, and never resolve a name")
|
||||
trunk = js[js.index("function trunkHaystack("):js.index("function filtering(")]
|
||||
for field in ("group.port", "listener.domains", "listener.clients"):
|
||||
assert field in trunk, f"the trunk no longer answers for {field}"
|
||||
|
||||
# (2) the repaint path: the payload in hand, no network, and no re-entry into the poll.
|
||||
panels = js[js.index("function renderPanels("):js.index("function pruneCards(")]
|
||||
assert "fetchJSON" not in panels and "refresh(" not in panels, (
|
||||
"the filter path dials something — it is an eye over `state.snapshot`, and a keystroke must "
|
||||
"cost no request (nor may it re-enter `refresh()`, which fetches a history pair per panel)")
|
||||
wiring = js[js.index("function initFilter("):js.index("function pruneCards(")]
|
||||
assert "state.filter = event.target.value.trim()" in wiring, \
|
||||
"the filter stopped taking its needle verbatim-and-trimmed from the field"
|
||||
assert "event.key !== 'Escape'" in wiring, \
|
||||
"Escape no longer clears the field, so getting out of a filter means backspacing it"
|
||||
assert "fetchJSON" not in wiring and "refresh(" not in wiring
|
||||
count = js[js.index("function renderFilterCount("):js.index("function initFilter(")]
|
||||
assert "`${standing} of ${total} routes`" in count, \
|
||||
"the chip stopped counting leaf routes the way the panels draw them"
|
||||
assert "chip.hidden = true" in count, \
|
||||
"the count stays on screen beside an empty field — ink about nothing"
|
||||
|
||||
# (3) the tallies and the warnings list never move, because neither is a route.
|
||||
for name, end in (("renderHeader", "async function refresh("),
|
||||
("renderWarnings", "function dotFor(")):
|
||||
body = js[js.index(f"function {name}("):js.index(end)]
|
||||
for leak in ("state.filter", "filtering(", "haystack(", "leafShown(", "trunkShown("):
|
||||
assert leak not in body, (
|
||||
f"{name} now reads {leak} — the header's four figures and the warnings count are the "
|
||||
"payload's own and must keep saying what the whole ingress declares")
|
||||
|
||||
# (4) rows are hidden, not removed, inside the patch pass that already holds them.
|
||||
leaves = js[js.index("function renderLeaves("):js.index("function renderGroups(")]
|
||||
assert "li.hidden = shown !== null && !shown[index]" in leaves, (
|
||||
"a leaf is no longer hidden by index into the group's own connection list — the one form "
|
||||
"that cannot credit a count to the wrong row")
|
||||
refusal = leaves[leaves.index("const emptyId"):]
|
||||
assert ".hidden" not in refusal, (
|
||||
"the trunk's refusal sentence is being hidden by a route's rules — it belongs to the trunk, "
|
||||
"so this fleet's `:80` is found by typing `80` and by nothing a route says")
|
||||
assert ".remove()" not in leaves, \
|
||||
"the filter deleted a row; removal belongs to a route leaving the config"
|
||||
groups = js[js.index("function renderGroups("):js.index("function familyWord(")]
|
||||
assert "el.hidden = !visible" in groups, "a trunk is no longer hidden as a whole"
|
||||
assert "const shown = trunk ? null : group.connections.map(leafShown)" in groups, (
|
||||
"a trunk that answered the query no longer keeps its whole fan-out — typing `4443` asks for "
|
||||
"the port, and every route on it is part of the answer")
|
||||
assert "const visible = !filtering() || trunk || shown.some(Boolean)" in groups, (
|
||||
"a group is no longer standing on \"the trunk or one of its routes\", which is the one rule "
|
||||
"that decides whether a port disappears from the map")
|
||||
assert "none.hidden = !stranded" in groups and \
|
||||
"const stranded = filtering() && groups.length > 0 && standing === 0" in groups, (
|
||||
"the panel's no-match sentence is no longer keyed on all three of: the filter is up, there "
|
||||
"were groups, and the query hid every one of them")
|
||||
assert '[data-k="no-match"]' in groups and 'no route matches' in groups, \
|
||||
"a panel whose every group is hidden now says nothing at all"
|
||||
|
||||
# (5) the capture, through the mirror: the four answers the field has to give.
|
||||
payload = gateway_client.get("/api/gateway/snapshot").json()
|
||||
total = len(payload["connections"])
|
||||
assert total > 10, "the capture routes too little for a filter to prove anything"
|
||||
|
||||
everything = filter_view(payload, "")
|
||||
assert everything["standing"] == total and not any(
|
||||
leaf["hidden"] for group in everything["groups"] for leaf in group["leaves"]), \
|
||||
"an empty query hides something — it must be the same page as no query at all"
|
||||
assert not any(group["hidden"] for group in everything["groups"]), \
|
||||
"an empty query hid a trunk"
|
||||
|
||||
brain = filter_view(payload, "brain")
|
||||
shown = [leaf["id"] for group in brain["groups"] for leaf in group["leaves"]
|
||||
if not leaf["hidden"]]
|
||||
assert brain["standing"] == len(shown) > 1, \
|
||||
"`brain` matched nothing but the brain routes"
|
||||
assert all("brain" in _leaf_haystack(next(c for c in payload["connections"]
|
||||
if c["id"] == leaf_id))
|
||||
for leaf_id in shown), "`brain` showed a route it does not appear in"
|
||||
assert all(c["id"] in shown for c in payload["connections"]
|
||||
if "brain" in _leaf_haystack(c)), "`brain` lost a route it appears in"
|
||||
assert any(group["port"] == 443 and not group["hidden"] for group in brain["groups"]), \
|
||||
"the trunk the brain routes hang off was hidden with the rest of its fan-out"
|
||||
assert brain["standing"] < total, "nothing was filtered out at all"
|
||||
|
||||
port4443 = filter_view(payload, "4443")
|
||||
for group in port4443["groups"]:
|
||||
if group["port"] == 4443:
|
||||
assert not group["hidden"] and all(
|
||||
not leaf["hidden"] for leaf in group["leaves"]), \
|
||||
":4443 did not keep its whole fan-out — typing a port asks for the port"
|
||||
else:
|
||||
assert group["hidden"], f":{group['port']} survived a query for :4443"
|
||||
|
||||
nothing = filter_view(payload, "no-such-domain-anywhere")
|
||||
assert nothing["standing"] == 0 and all(group["hidden"] for group in nothing["groups"]), \
|
||||
"a query that matches nothing left a row or a trunk on the page"
|
||||
|
||||
# (6) a trunk answers for **itself**: a client address is the listener's own payload, and typing
|
||||
# it is a question about the port, so every route under it stays as part of the answer.
|
||||
client_row = next((row for row in payload["listeners"] if row["clients"]), None)
|
||||
assert client_row, "the captured socket table names no client, so this arm cannot be exercised"
|
||||
by_port = filter_view(payload, client_row["clients"][0])
|
||||
home = next(group for group in by_port["groups"] if group["port"] == client_row["port"])
|
||||
assert home["trunk_hit"] and not home["hidden"], \
|
||||
f"typing {client_row['clients'][0]!r} did not match :{client_row['port']}'s own row"
|
||||
assert all(not leaf["hidden"] for leaf in home["leaves"]), \
|
||||
"a trunk that matched lost leaves of its fan-out"
|
||||
|
||||
|
||||
def test_the_filter_field_is_markup_the_sheet_and_nobody_else_uses(client) -> None:
|
||||
"""The field is pinned markup, on one page, wearing the sheet's one field skin.
|
||||
|
||||
Three absences carry as much as the two presences. **No other page ships `type="search"`**: the
|
||||
shared rule is what makes a field match the theme, and a page growing its own styled box is how a
|
||||
control stops following a theme change. The base rule is `max-width: 18rem; min-width: 0` and the
|
||||
≤640 px block makes it `width: 100%` — a 33-name ingress is exactly the case for typing into the
|
||||
field on a phone, and a field that keeps 18 rem of cap there is a field with two characters of
|
||||
visible text. And `.gate-filter` carries no `:hover`, no `cursor` and no `transition`: the
|
||||
connection map owns one interactive-looking thing (the unwired switch's knob) and this is not it.
|
||||
"""
|
||||
css = _css(client)
|
||||
shared = _declarations(css, 'input[type="text"], input[type="password"], input[type="search"]')
|
||||
for prop in ("background", "color", "border", "border-radius", "padding", "font"):
|
||||
assert shared.get(prop), f"the shared field rule no longer declares {prop}"
|
||||
assert shared.get("border") == "1px solid var(--border)", \
|
||||
"the field skin drifted from the sheet's own border token"
|
||||
|
||||
section = _code(_css_section(css, "gateway pipes"))
|
||||
base = _declarations(section, ".gate-filter")
|
||||
assert base.get("max-width") == "18rem", \
|
||||
f"the field is capped at {base.get('max-width')!r} rather than 18rem"
|
||||
assert base.get("min-width") == "0", \
|
||||
"without `min-width: 0` the field cannot shrink, so a long placeholder grows the page head"
|
||||
|
||||
# The phone override lives in the gateway section's own ≤640 px block and says only that one thing.
|
||||
mobile = section[section.index("@media (max-width: 640px)"):]
|
||||
reach = re.search(r"\.gate-filter\s*\{([^}]*)\}", mobile)
|
||||
assert reach, "the field is not full-width at the phone break — 33 domains want a real field"
|
||||
wide = _declarations(mobile, ".gate-filter")
|
||||
assert wide.get("width") == "100%", f"the phone rule reads {wide} rather than `width: 100%`"
|
||||
assert wide.get("max-width") == "none", (
|
||||
f"{wide} lifts the width but not the 18rem cap, so the field is still 288 px on a 320 px page")
|
||||
|
||||
for selector, body in re.findall(r"([^{}]*\.gate-filter[^{}]*)\{([^}]*)\}", _code(css)):
|
||||
assert ":hover" not in selector, f"the filter grew a hover affordance: {selector.strip()}"
|
||||
assert "cursor" not in body and "transition" not in body, (
|
||||
f"the field advertises itself as a pointer control: {body.strip()}")
|
||||
|
||||
pages = {path: client.get(path).text for path in (*MARKUP_PAGES, "/login")}
|
||||
html = pages[GATEWAY_PAGE]
|
||||
head = html[html.index("Connection map"):]
|
||||
field = next(tag for tag in re.findall(r"<input[^>]*>", head) if 'id="gate-filter"' in tag)
|
||||
chip = next(tag for tag in re.findall(r"<span[^>]*>", head) if 'id="gate-filter-count"' in tag)
|
||||
for attr in ('type="search"', 'class="gate-filter"', 'autocomplete="off"', 'spellcheck="false"'):
|
||||
assert attr in field, f"the filter field lost {attr}"
|
||||
assert 'aria-label="Filter routes by domain, endpoint or port"' in field, \
|
||||
"the field has no accessible name — a placeholder is a hint, not a label"
|
||||
assert "placeholder=" in field, "the field no longer says what it matches on"
|
||||
assert 'class="chip faint"' in chip and "hidden" in chip \
|
||||
and 'aria-live="polite"' in chip, f"the count chip is {chip}"
|
||||
for other, page in pages.items():
|
||||
if other != GATEWAY_PAGE:
|
||||
assert 'type="search"' not in page, f"{other} grew a second search field"
|
||||
|
||||
# one no-match sentence per panel, and it ships hidden like every conditional sentence here
|
||||
for key in ("nginx", "haproxy"):
|
||||
card = html[html.index(f'data-gate="{key}"'):]
|
||||
card = card[:card.index("</article>")]
|
||||
assert card.count('data-k="no-match"') == 1, \
|
||||
f"the {key} panel has no single no-match sentence"
|
||||
assert 'data-k="no-match" hidden' in card, f"the {key} no-match sentence ships visible"
|
||||
assert 'data-k="empty"' in card, \
|
||||
f"the {key} panel lost the sentence for a config that names no port — the filter's " \
|
||||
"sentence is a different fact"
|
||||
|
||||
# the footer says what the field does and does not touch, once
|
||||
foot = " ".join(html[html.index("<footer class="):].split())
|
||||
assert "hides the routes whose own labels do not contain what you typed" in foot, \
|
||||
"the page stopped saying out loud that the filter asks for nothing"
|
||||
assert "the tallies, the warnings and the charts above it are still the whole ingress" in foot, \
|
||||
"the sentence no longer names the three things a filter must not quietly narrow"
|
||||
for junk in ("no page narrates", "every value is scraped", "filters live, nothing is stored"):
|
||||
assert junk not in foot, f"the footer grew flavour text: {junk}"
|
||||
|
||||
|
||||
def test_haproxy_draws_straight_across_and_nginx_branches(client) -> None:
|
||||
"""The geometry the operator asked for, in four rules and zero JavaScript.
|
||||
|
||||
@@ -6157,8 +6715,12 @@ def test_haproxy_draws_straight_across_and_nginx_branches(client) -> None:
|
||||
before = _declarations(section, ".gate-link::before")
|
||||
after = _declarations(section, ".gate-link::after")
|
||||
for name, rule in (("::before", before), ("::after", after)):
|
||||
assert rule.get("border-left") == "2px dashed var(--border-strong)", \
|
||||
f"the spine's {name} half lost its dashed border"
|
||||
# `transparent`, not a colour: this rule is the branch's *geometry*, and the ink arrives from
|
||||
# the row's state class (`test_the_whole_route_takes_one_existence_class`). A colour here would
|
||||
# be a row drawing a confident line before any payload said the route exists.
|
||||
assert rule.get("border-left") == "2px dashed transparent", (
|
||||
f"the spine's {name} half no longer declares a dashed, colourless border — geometry "
|
||||
"belongs on the row and colour belongs to `.gate-link.on`/`.off`")
|
||||
assert rule.get("position") == "absolute" and rule.get("left") == "0", \
|
||||
f"the spine's {name} half no longer sits in the row's gutter"
|
||||
assert before.get("top") == "calc(var(--gate-leaf-gap) / -2)", \
|
||||
@@ -6175,8 +6737,10 @@ def test_haproxy_draws_straight_across_and_nginx_branches(client) -> None:
|
||||
"`--gate-leaf-gap` is no longer declared in `:root`"
|
||||
assert _declarations(section, ".gate-link:first-child::before, .gate-link:last-child::after").get(
|
||||
"display") == "none", "the fan-out grew a spine past its first and last branch"
|
||||
assert "2px dashed" in _declarations(section, ".gate-elbow").get("border-top", ""), \
|
||||
"the tick from the spine to the switch is gone, so a leaf no longer reaches its trunk"
|
||||
elbow = _declarations(section, ".gate-elbow").get("border-top", "")
|
||||
assert "2px dashed" in elbow and elbow.endswith("transparent"), (
|
||||
"the tick from the spine to the switch is gone, or coloured itself, so a leaf no longer "
|
||||
"reaches its trunk in the same ink as its run")
|
||||
|
||||
solo = _declarations(
|
||||
section, ".gate-group.solo .gate-link::before, .gate-group.solo .gate-link::after")
|
||||
@@ -6221,6 +6785,14 @@ def test_the_pipe_animation_stills_in_both_states_under_reduced_motion(client) -
|
||||
Phase 30 task 05 added the third name, and it is not decoration: the switch's knob slides on a
|
||||
`transition` of its own, and a block that stills the pipes while the thumb keeps sliding is the
|
||||
phase-18 lesson half-applied — the operator asked for no motion, not for less of it.
|
||||
|
||||
Phase 31 task 03 added a **second pipe** (the trunk's stub) and deliberately added no fourth name.
|
||||
That works only because of how the two names are written, so it is pinned rather than assumed: the
|
||||
selectors are **descendant** (`.pipe.live .pipe-path`), and the stub is built by the same
|
||||
`buildPipe(trunk)` with the same `pipe pipe-trunk` class list and the same `.pipe-path` child. A
|
||||
rule that reaches `.pipe.live .pipe-path` therefore reaches a trunk's path too. The failure this
|
||||
prevents is the quiet one: someone gives `.pipe-trunk .pipe-path` its own `animation` to make the
|
||||
stub "nicer", and the block below stops covering it.
|
||||
"""
|
||||
code = _code(_css(client))
|
||||
block = re.search(r"@media\s*\(prefers-reduced-motion:\s*reduce\)\s*\{(.*?)\n\}",
|
||||
@@ -6236,16 +6808,36 @@ def test_the_pipe_animation_stills_in_both_states_under_reduced_motion(client) -
|
||||
"the named rules stopped stilling the animation"
|
||||
assert _declarations(block.group(1), ".gate-switch-thumb").get("transition") == "none", \
|
||||
"the switch's knob still slides when the operator asked for no motion"
|
||||
# The two names are descendant selectors, which is the whole reason a second pipe needs no third:
|
||||
# `.pipe.live` is on the `<svg>` (leaf or trunk) and `.pipe-path` is its child in both cases.
|
||||
assert all(name.endswith(" .pipe-path") for name in named), (
|
||||
f"the stilling rules are {sorted(named)} — a rule aimed at `.pipe.live` alone would not "
|
||||
"reach the `<path>` that carries the `animation`, and would not reach a trunk's either")
|
||||
|
||||
# …and every animated thing on the page is in this block, counted rather than trusted: a third
|
||||
# `transition` added to the map later has to be named here, not assumed to be covered.
|
||||
animated = {".pipe.live .pipe-path", ".pipe.still .pipe-path", ".gate-switch-thumb"}
|
||||
section = _code(_css_section(_css(client), "gateway pipes"))
|
||||
transitioned = {" ".join(sel.split()) for sel, body in
|
||||
re.findall(r"([^{}]+)\{([^}]*)\}", _code(_css_section(_css(client),
|
||||
"gateway pipes")))
|
||||
re.findall(r"([^{}]+)\{([^}]*)\}", section)
|
||||
if "transition:" in body or "animation:" in body}
|
||||
assert transitioned <= animated, (
|
||||
f"{sorted(transitioned - animated)} animates in the gateway map without being stilled")
|
||||
|
||||
# The trunk's stub must earn that coverage rather than assume it: it exists because `buildPipe`
|
||||
# makes it (one class shape for both lines) and it owns no motion declaration of its own.
|
||||
js = _code(client.get("/static/page-gateway.js").text)
|
||||
assert "function buildPipe(trunk = false)" in js and "'pipe pipe-trunk'" in js, \
|
||||
"`.pipe-trunk` is no longer produced by the one pipe builder, so it is a second component "\
|
||||
"with a class shape this block does not reach"
|
||||
stub_rules = [body.strip() for sel, body in re.findall(r"([^{}]+)\{([^}]*)\}", section)
|
||||
if ".pipe-trunk" in sel]
|
||||
assert stub_rules, "the stylesheet lost the trunk stub's rule"
|
||||
for body in stub_rules:
|
||||
assert "animation" not in body and "transition" not in body, (
|
||||
f"the trunk stub styles motion itself ({body!r}), which the stilling block above does "
|
||||
"not name — motion belongs on `.pipe.live .pipe-path` and nowhere else")
|
||||
|
||||
|
||||
def test_no_gateway_count_is_coloured_by_a_verdict(client) -> None:
|
||||
"""The pipes and switches colour from **existence**, and the verdict vocabulary stays out.
|
||||
@@ -6275,6 +6867,18 @@ def test_no_gateway_count_is_coloured_by_a_verdict(client) -> None:
|
||||
section = _code(_css_section(_css(client), "gateway pipes"))
|
||||
for token in ("--ok", "--warn", "--fail", ".ok", ".warn", ".fail", "severity"):
|
||||
assert token not in section, f"the gateway stylesheet section paints with {token}"
|
||||
# Phase 31 task 01 extended that single vocabulary to the **whole** row, and the two halves of
|
||||
# this assertion are the same claim read two ways: every connector takes the existence pair, and
|
||||
# nothing in this section any longer has a reason to name the structure token. `--border-strong`
|
||||
# was where the spine and the tick lived while only the run went green, so its absence here *is*
|
||||
# the statement "a route is green from its trunk to its endpoint, or grey for all of it".
|
||||
assert "--border-strong" not in section, (
|
||||
"a gateway connector reads structure ink again — the spine halves, the tick, the run and the "
|
||||
"switch are one existence claim and must take `--gate-on`/`--gate-off` together")
|
||||
for connector in (".gate-link.on::before", ".gate-link.on::after", ".gate-link.on .gate-elbow",
|
||||
".gate-link.off::before", ".gate-link.off::after",
|
||||
".gate-link.off .gate-elbow"):
|
||||
assert connector in section, f"the row's state class no longer paints {connector}"
|
||||
js = _code(client.get("/static/page-gateway.js").text)
|
||||
for helper in ("severityFrom", "severityFor", ".severity"):
|
||||
assert helper not in js, f"/gateway grades a number with {helper}"
|
||||
@@ -6365,12 +6969,14 @@ def test_the_page_reports_no_card_and_no_cell_for_sockets_the_configs_do_not_nam
|
||||
|
||||
|
||||
def test_the_page_says_where_a_line_comes_from_and_where_its_count_cannot(client) -> None:
|
||||
"""Three footnotes, one per claim the page is allowed to make, and not one more.
|
||||
"""Four footnotes, one per claim the page is allowed to make, and not one more.
|
||||
|
||||
Why the page draws a line at all (the config declares the route) and what its motion means (a
|
||||
socket is open this instant, so a resting line is not a dead one); why an endpoint written as a
|
||||
**name** carries no number (nothing here resolves one, so a dash is not a fault); and what the
|
||||
switch does **not** do (toggling a route needs a config write path this app does not have).
|
||||
**name** carries no number (nothing here resolves one, so a dash is not a fault); what the switch
|
||||
does **not** do (toggling a route needs a config write path this app does not have); and — since
|
||||
phase 31 task 04 — what the filter box does to the map and what it leaves alone (the tallies, the
|
||||
warnings and the charts keep describing the whole ingress, because the field asks for nothing).
|
||||
Each lives in the footer exactly once — the page does not repeat a why-line per panel, and it
|
||||
does not need to: an uncounted line prints `—` and the footnote is the sentence that explains it
|
||||
(rule 2).
|
||||
@@ -6381,8 +6987,8 @@ def test_the_page_says_where_a_line_comes_from_and_where_its_count_cannot(client
|
||||
"the no-DNS rule is the page's single footnote about a missing count — printed once")
|
||||
assert "animates" in footer, "the animation rule lost its footnote"
|
||||
band = footer[:footer.index("</footer>")]
|
||||
# Three paragraphs and nothing else: one sentence per claim, in one ink band under the panels.
|
||||
assert band.count("<p") == 3, f"the footer is {band.count('<p')} paragraphs, not three"
|
||||
# Four paragraphs and nothing else: one sentence per claim, in one ink band under the panels.
|
||||
assert band.count("<p") == 4, f"the footer is {band.count('<p')} paragraphs, not four"
|
||||
assert "<span" not in band, "the footer grew a second element inside a sentence"
|
||||
assert "the config says the route exists" in band, "the page stopped saying what a line means"
|
||||
assert "not wired up" in band and "write" in band, \
|
||||
@@ -6466,8 +7072,43 @@ def test_the_gateway_page_keeps_its_last_drawing_when_a_poll_fails(client) -> No
|
||||
"and the reason has to be printed where the operator is already looking"
|
||||
|
||||
|
||||
def test_a_clipped_pipe_label_keeps_its_full_text_on_the_element(client) -> None:
|
||||
"""The ellipsis is only honest while the whole string is on the element, and pinned so.
|
||||
def rendered_leaf(row: dict[str, Any]) -> dict[str, str]:
|
||||
"""The two lines and the two long strings one leaf row prints, derived as a **second** reading.
|
||||
|
||||
There is no JS runner (a locked anchor), so this is `endpoint_sums`' idiom: the figure is
|
||||
recomputed here from the payload, and the served-byte pins are what keep the mirror honest —
|
||||
`inboundText` joins `out.selectors` and falls back to `out.kind`, `endpointText` is `out.host`
|
||||
+ `:` + `out.port`, `counterpartText` is `ARROW` + endpoint + ` · out.kind`, and `leafTitle` /
|
||||
`leafAria` compose those same two from the literals asserted verbatim below. A rename on either
|
||||
side therefore fails a pin **and** changes what this mirror prints, which is the pair of
|
||||
guarantees a page with no test runner can actually make.
|
||||
"""
|
||||
out = row["out"]
|
||||
host = out["host"] or "?"
|
||||
port = out["port"]
|
||||
endpoint = f"{host}{'' if port is None else f':{port}'}"
|
||||
kind = out["kind"]
|
||||
inbound = " ".join(out["selectors"]) or kind
|
||||
pair = (f"{inbound} " if inbound else "") + f"→ {endpoint}" + (f" · {kind}" if kind else "")
|
||||
shared = " — this endpoint is shared with another route" if row["shared"] is True else ""
|
||||
bits = [f"connection from :{row['in']['port']} to {endpoint}"]
|
||||
if out["selectors"]:
|
||||
bits.append("reached by " + ", ".join(out["selectors"]))
|
||||
if out["name"]:
|
||||
bits.append(f"server {out['name']}")
|
||||
if row["shared"] is True:
|
||||
bits.append("this endpoint is shared with another route")
|
||||
bits.append("in the config")
|
||||
bits.append("connections not attributed" if row["connections"] is None
|
||||
else f"{row['connections']} live connections")
|
||||
return {"line1": inbound,
|
||||
"line2": f"→ {endpoint}" + (f" · {kind}" if kind else ""),
|
||||
"title": pair + shared,
|
||||
"aria": ", ".join(bits)}
|
||||
|
||||
|
||||
def test_a_clipped_pipe_label_keeps_its_full_text_on_the_element(client, gateway_client) -> None:
|
||||
"""The ellipsis is only honest while **both ends** are on the element, and pinned so.
|
||||
|
||||
Phase 29 task 06 measured 28 of 42 labels ellipsised at 1440 px and found the old fix (letting the
|
||||
label wrap) worse than the clip — the flex line squeezed the name to seven characters of a
|
||||
@@ -6476,17 +7117,29 @@ def test_a_clipped_pipe_label_keeps_its_full_text_on_the_element(client) -> None
|
||||
endpoint labels clip at 1440 px (a 391 px target column) and **27 of 33** do at 360 px (177 px), so
|
||||
what remains is a phone column-width fact (AGENTS.md gap 13) and not a lost one.
|
||||
|
||||
What makes the ellipsis a truncation and not a lost fact is that the row carries the whole string
|
||||
— endpoint *and* the config's own selector — as its `title`, and the line's `aria-label` carries
|
||||
the full endpoint whatever the eye can see. Both halves are pinned here, because the day either
|
||||
one goes the ellipsis starts eating the page's answer in silence. Task 06 re-counts the clipped
|
||||
labels on the new row shape.
|
||||
Phase 31 task 02 made that sentence twice as important, because the row now prints two strings and
|
||||
both are the answer: `brain.reeseapps.com` on line 1 and
|
||||
`→ reeseapps-brain-of-reese.reeselink.com:443` on line 2. So the claim is strengthened from "the
|
||||
target survives the clip" to **every inbound word and the whole `host:port` appear in full in the
|
||||
row's `title` *and* in the pipe's `aria-label`**, for every route the capture declares. Three
|
||||
things make that true rather than aspirational: neither text helper cuts a string (no `slice`, no
|
||||
`substring`, no ellipsis character — an ellipsis is CSS's job and CSS does not touch `title`),
|
||||
`leafTitle` composes `inboundText` with `endpointText`, and `leafAria` names the endpoint and every
|
||||
selector. The payload half then runs that over the real rows, longest name included.
|
||||
"""
|
||||
css = _code(_css(client))
|
||||
assert _declarations(css, ".pipe-name").get("text-overflow") == "ellipsis", \
|
||||
".pipe-name no longer truncates, so re-measure the wrap decision this pin documents"
|
||||
assert _declarations(css, ".gate-selector").get("text-overflow") == "ellipsis", \
|
||||
"the selector line no longer truncates — the row's tooltip is what makes that honest"
|
||||
"the trunk's name line no longer truncates — its `title` is what makes that honest"
|
||||
# …and the leaf's counterpart line, which is now the longer of the row's two strings and so the
|
||||
# one that actually clips. Same trio, same reason: a truncation is only honest while `title` and
|
||||
# the `aria-label` below carry the whole thing.
|
||||
end = _declarations(css, ".pipe-end")
|
||||
assert end.get("text-overflow") == "ellipsis", \
|
||||
".pipe-end no longer truncates — re-measure which of the row's two lines clips (gap 13)"
|
||||
assert end.get("min-width") == "0" and end.get("white-space") == "nowrap", \
|
||||
".pipe-end lost part of the truncation trio, so a long counterpart grows the card"
|
||||
# …and the two declarations that decide *how much* it truncates. Task 06 measured the old page
|
||||
# showing seven characters of a 40-character target, because a `nowrap` flex row shrinks the name
|
||||
# to what the chip, the footnote and the `flex: none` count leave. `max-content` as its base size
|
||||
@@ -6504,15 +7157,155 @@ def test_a_clipped_pipe_label_keeps_its_full_text_on_the_element(client) -> None
|
||||
|
||||
js = _code(client.get("/static/page-gateway.js").text)
|
||||
row = js[js.index("function renderLeaf("):js.index("function groupElement(")]
|
||||
assert "const text = endpointText(conn.out)" in row, \
|
||||
"the label and the tooltip must be built from one string, not two"
|
||||
assert "const inbound = inboundText(conn.out)" in row, \
|
||||
"the row no longer builds both of its strings from one pair of helpers"
|
||||
assert "li.title = title" in row, "a clipped connection line lost the full text on the element"
|
||||
title = js[js.index("function leafTitle("):js.index("function buildPipe(")]
|
||||
assert "endpointText(conn.out)" in title and "selectorText(conn.out)" in title, \
|
||||
"the row's tooltip stopped carrying the endpoint and the config's own selector"
|
||||
assert "inboundText(out)" in title and "endpointText(out)" in title, \
|
||||
"the row's tooltip stopped carrying one of the row's two ends — half a route is no answer"
|
||||
aria = js[js.index("function leafAria("):js.index("function buildPipe(")]
|
||||
assert "endpointText(out)" in aria, \
|
||||
"the accessible sentence stopped carrying the whole endpoint a clipped label truncates"
|
||||
assert "endpointText(out)" in aria and "selectors.join" in aria, \
|
||||
"the accessible sentence stopped carrying the whole endpoint or every inbound word"
|
||||
# Nothing in the row's text helpers may cut a string. The CSS ellipsis is honest only because the
|
||||
# data behind it is whole, and a `slice(0, 30)` in `counterpartText` would eat the tooltip too.
|
||||
helpers = js[js.index("function endpointText("):js.index("function buildPipe(")]
|
||||
for cutter in ("slice(", "substring(", "substr(", "trim(", "\u2026"):
|
||||
assert cutter not in helpers, (
|
||||
f"a leaf's text helpers cut strings with {cutter!r} — truncation belongs to CSS so the "
|
||||
"`title` and the `aria-label` can carry the whole route (gap 13)")
|
||||
|
||||
# …and the claim over every route the captured ingress actually declares, longest name first:
|
||||
# `reeseapps-brain-of-code.reeselink.com:443`, 41 characters, is this fleet's longest endpoint and
|
||||
# the one the 17-row `:443` fan-out clips at a phone width.
|
||||
rows = gateway_client.get("/api/gateway/snapshot").json()["connections"]
|
||||
assert rows, "the captured ingress declares no routes, so this pin would prove nothing"
|
||||
longest = max(rows, key=lambda r: len(f"{r['out']['host']}:{r['out']['port']}"))
|
||||
longest_text = f"{longest['out']['host']}:{longest['out']['port']}"
|
||||
# 42 characters, which is the figure AGENTS.md gap 13 measures the phone clip against. The name
|
||||
# itself is pinned as a family rather than a string because this fleet has two 41/42-character
|
||||
# neighbours (`…-brain-of-reese` and `…-brain-of-code`) and which one is longest is a fact about
|
||||
# `stream.conf`, not a contract; the **length** is the thing the clip depends on.
|
||||
assert len(longest_text) == 42 and longest["out"]["host"].startswith("reeseapps-brain-of-"), (
|
||||
f"the fleet's longest endpoint is now {longest_text!r} ({len(longest_text)} chars) — gap 13's "
|
||||
"clip measurement and gap 13's prose both have to be re-read")
|
||||
for conn in rows:
|
||||
printed = rendered_leaf(conn)
|
||||
whole = f"{conn['out']['host']}:{conn['out']['port']}"
|
||||
for surface in ("title", "aria"):
|
||||
assert whole in printed[surface], (
|
||||
f"{whole} is not whole in the row's {surface} — gap 13: whatever CSS clips, the "
|
||||
"element has to carry the full counterpart")
|
||||
for selector in conn["out"]["selectors"]:
|
||||
assert selector in printed["title"] and selector in printed["aria"], (
|
||||
f"the inbound word {selector!r} is missing from a surface that is supposed to survive "
|
||||
"the clip — the row's answer is *which domain goes where*, so both ends are needed")
|
||||
|
||||
|
||||
def test_the_leaf_row_leads_with_the_inbound_word(client, gateway_client) -> None:
|
||||
"""Line 1 is the domain the operator types; line 2 is the counterpart it flows to.
|
||||
|
||||
The operator's complaint (2026-10-23): *"I can't tell which `reeseapps.com` or
|
||||
`stackexpected.com` addresses flow to their reeselink counterparts (very important)."* Phase 30
|
||||
drew the **target** on the primary line and demoted the inbound `server_name` / `map` key to an
|
||||
11px faint qualifier, which is exactly backwards from the question the page exists to answer. So
|
||||
the row re-ranked: `.pipe-name` is the config's inbound word and `.pipe-end` is
|
||||
`→ host:port · kind` underneath it.
|
||||
|
||||
Three pins keep that from being a cosmetic swap that quietly invents something:
|
||||
|
||||
* `inboundText` reads `out.selectors` and falls back to `out.kind` — **never** `out.host`, because
|
||||
picking the target as the "best name" is a fact the two config files never stated (rule 2);
|
||||
* all the selectors are joined, so a merged connection (two vhosts claiming one
|
||||
`(in_port, endpoint)`) shows both rather than the page choosing one;
|
||||
* every field the two helpers index is in `GATEWAY_PAGE_FIELDS` **and** resolves on a live
|
||||
`GET /api/gateway/snapshot` row, in both directions, so a rename in `sources/gateway.py` fails
|
||||
this test by name instead of printing `undefined` beside a healthy ingress.
|
||||
|
||||
The last block is the complaint itself, asserted on the capture: the `brain.reeseapps.com` leaf of
|
||||
nginx's `:443` reads that domain on line 1 and
|
||||
`reeseapps-brain-of-reese.reeselink.com:443` on line 2, every haproxy leaf leads with its `backend`
|
||||
name, and no row's primary line is empty, `null` or `undefined`.
|
||||
"""
|
||||
js = _code(client.get("/static/page-gateway.js").text)
|
||||
|
||||
# (1) the two text helpers, and the one field each may never read.
|
||||
head = js[:js.index("function countText(")]
|
||||
assert "const ARROW = '\u2192'" in head, "the destination marker is no longer a page constant"
|
||||
inbound = js[js.index("function inboundText("):js.index("function counterpartText(")]
|
||||
assert "(out || {}).selectors" in inbound and ".join(' ')" in inbound, \
|
||||
"line 1 stopped printing every selector the config wrote"
|
||||
assert "(out || {}).kind" in inbound, \
|
||||
"a row with no selector has no fallback, so its primary line would be empty"
|
||||
assert "host" not in inbound and "port" not in inbound, \
|
||||
"line 1 reaches for the target again — the counterpart is line 2's job and nothing else"
|
||||
counterpart = js[js.index("function counterpartText("):js.index("function leafAria(")]
|
||||
assert "${ARROW} ${endpointText(out)}" in counterpart, \
|
||||
"line 2 stopped marking the endpoint as the destination"
|
||||
assert "\u00b7 ${kind}" in counterpart, \
|
||||
"line 2 lost the config's own word for the route kind, so a `map` leaf and a vhost leaf "\
|
||||
"reaching one address became indistinguishable"
|
||||
|
||||
# (2) the call sites: line 1 is the inbound word, line 2 the counterpart, and the primary line
|
||||
# is never handed the endpoint as its *first* choice.
|
||||
row = js[js.index("function renderLeaf("):js.index("function groupElement(")]
|
||||
assert "const inbound = inboundText(conn.out)" in row
|
||||
assert "const primary = inbound == null ? endpointText(conn.out) : inbound" in row, \
|
||||
"a row with no selector would print an empty primary line instead of its endpoint"
|
||||
assert "name.textContent = primary" in row
|
||||
assert not re.search(r"name\.textContent\s*=\s*endpointText", row), \
|
||||
"the endpoint is back on the primary line — that is the reading order this task replaced"
|
||||
assert "setTxt(counterpart, counterpartText(conn.out))" in row, \
|
||||
"line 2 is no longer the counterpart"
|
||||
assert "counterpart.hidden = inbound == null" in row, \
|
||||
"the hidden rule no longer keys on the fallback, so a row could print its target twice"
|
||||
markup = js[js.index("function leafRow("):js.index("function renderLeaf(")]
|
||||
assert "counterpart.className = 'pipe-end mono'" in markup, \
|
||||
"line 2 is no longer mono, so the two hostname columns stopped aligning down a fan-out"
|
||||
assert "target.appendChild(name)" in markup and "target.appendChild(counterpart)" in markup, \
|
||||
"the two lines are no longer both children of the target column"
|
||||
|
||||
# (3) the field table resolves in both directions for what the two helpers index.
|
||||
needed = ("connections.out.selectors", "connections.out.kind",
|
||||
"connections.out.host", "connections.out.port")
|
||||
for path in needed:
|
||||
assert path in GATEWAY_PAGE_FIELDS, (
|
||||
f"the leaf row indexes {path.removeprefix('connections.')} but `GATEWAY_PAGE_FIELDS` "
|
||||
"does not name it, so a rename in `sources/gateway.py` would print `undefined` here")
|
||||
payload = gateway_client.get("/api/gateway/snapshot").json()
|
||||
rows = payload["connections"]
|
||||
assert rows, "the capture routes nothing, so the field table would resolve over an empty list"
|
||||
for path in needed:
|
||||
found, _values = _payload_field(payload, path)
|
||||
assert found, f"/gateway's row reads {path}, which the payload does not publish"
|
||||
|
||||
# (4) the complaint, on the capture: `brain.reeseapps.com` → its reeselink counterpart.
|
||||
brain = [conn for conn in rows if conn["out"]["selectors"] == ["brain.reeseapps.com"]]
|
||||
assert brain, "the captured stream `map` no longer routes brain.reeseapps.com"
|
||||
printed = rendered_leaf(brain[0])
|
||||
assert printed["line1"] == "brain.reeseapps.com", \
|
||||
f"line 1 reads {printed['line1']!r}, not the inbound domain the operator reads for"
|
||||
assert "reeseapps-brain-of-reese.reeselink.com:443" in printed["line2"], \
|
||||
f"line 2 reads {printed['line2']!r} — the counterpart is what the row exists to show"
|
||||
assert brain[0]["in"]["port"] == 443 and brain[0]["source"] == "nginx", \
|
||||
"that leaf no longer hangs off the stream `map`'s port in the nginx panel"
|
||||
haproxy = [conn for conn in rows if conn["source"] == "haproxy"]
|
||||
assert haproxy, "the capture's haproxy routes are gone, so this half proves nothing"
|
||||
for conn in haproxy:
|
||||
assert rendered_leaf(conn)["line1"] == conn["out"]["selectors"][0], \
|
||||
"a haproxy leaf no longer leads with its `backend` name"
|
||||
assert any(rendered_leaf(c)["line1"] == "default" for c in rows), \
|
||||
"the map's own word for unmapped SNI was paraphrased off the primary line"
|
||||
for conn in rows:
|
||||
line1 = rendered_leaf(conn)["line1"]
|
||||
assert line1 and line1 not in ("null", "undefined", "None"), (
|
||||
f"a row's primary line prints {line1!r} — every route has an inbound word, and an empty "
|
||||
"primary line is the bug the fallback exists to prevent")
|
||||
|
||||
# (5) the trunk is untouched by the re-rank: it still leads with its port and still refuses in
|
||||
# the script's own sentence when its config names no target (this fleet's `:80` `return 301`).
|
||||
trunk = js[js.index("function renderTrunk("):js.index("function renderLeaves(")]
|
||||
assert "name.textContent !== port" in trunk, "the trunk no longer prints its port as line 1"
|
||||
assert "no target named in the config" in js, "the `:80` refusal sentence is gone"
|
||||
|
||||
|
||||
def test_the_switch_is_built_and_explicitly_unwired(client) -> None:
|
||||
@@ -6575,10 +7368,24 @@ def test_the_switch_is_built_and_explicitly_unwired(client) -> None:
|
||||
for fact in ("endpointText(conn.out)", "selectors", "config", "on ? 'on' : 'off'"):
|
||||
assert fact in label, f"the switch's name stopped saying {fact}"
|
||||
|
||||
# The events this page listens for, as an allow-list rather than a count: `toggle` is the charts
|
||||
# disclosure, `DOMContentLoaded` is the boot, and the two phase-31 names belong to the filter
|
||||
# field and to nothing else. **`click` is not on the list** and that is the switch's pin — the day
|
||||
# a route can be toggled, some code has to dial a config write, and invariant 1 says this app has
|
||||
# no such thing.
|
||||
bound = set(re.findall(r"addEventListener\('(\w+)'", js))
|
||||
assert bound <= {"toggle", "DOMContentLoaded"}, (
|
||||
assert bound <= {"toggle", "DOMContentLoaded", "input", "keydown"}, (
|
||||
f"the page now binds {sorted(bound)} — a switch that could be clicked would need a config "
|
||||
"write path this app does not have (invariant 1)")
|
||||
assert "click" not in bound, \
|
||||
"a click handler appeared on /gateway: nothing on this page may write to the fleet"
|
||||
filter_body = js[js.index("function initFilter("):js.index("function pruneCards(")]
|
||||
for event in ("input", "keydown"):
|
||||
assert f"addEventListener('{event}'" in filter_body, (
|
||||
f"the filter no longer binds {event} in its own function, so the allow-list above is "
|
||||
"hiding a listener somewhere it cannot be read")
|
||||
assert js.count(f"addEventListener('{event}'") == 1, \
|
||||
f"{event} is bound in more than one place on this page"
|
||||
assert not re.search(r"gate-switch[^\n]*\)\s*\.?\s*addEventListener", js), \
|
||||
"a listener was bound to the switch itself"
|
||||
|
||||
|
||||
Reference in New Issue
Block a user