phase: 32_gateway_resolved_target_liveness

Fixture and live stream.conf are byte-identical (modulo header comment). Let me test the parser directly:
This commit is contained in:
2026-10-26 14:03:42 -04:00
parent 25fd74b7c5
commit 51772ffef2
58 changed files with 5272 additions and 478 deletions
+93 -6
View File
@@ -21,6 +21,7 @@ a unit test.
from __future__ import annotations
import asyncio
import ipaddress
import json
import os
import sqlite3
@@ -44,6 +45,31 @@ from ups_contract import assert_ups_hero_contract
pytestmark = pytest.mark.live
def endpoint_identity(out: Any) -> tuple[Any, ...] | None:
"""One endpoint in the kernel's currency, derived **here** rather than by the source.
An address endpoint is its own address; a name endpoint is the addresses the agent's resolver
published for it (`out.resolved`), normalised so `::ffff:10.1.2.64` and `10.1.2.64` are one
key. `None` is "nothing can name this far end", which is counted nowhere. Phase 32 made this
helper necessary: a live router can have two route lines reaching one address through two
different names, and a total keyed on the config's text would book it twice.
"""
port = out.get("port")
if not isinstance(port, int):
return None
spellings = ([out["host"]] if out.get("address") is True
else list(out.get("resolved") or []))
keys: list[str] = []
for spelling in spellings:
try:
address = ipaddress.ip_address(str(spelling))
except ValueError:
continue
mapped = getattr(address, "ipv4_mapped", None)
keys.append(str(mapped if mapped is not None else address))
return (min(keys), port) if keys else None
@pytest.fixture(scope="module")
def settings() -> Settings:
"""Live env, not the import-time SETTINGS singleton."""
@@ -270,6 +296,11 @@ async def test_the_gateway_source_joins_the_live_ingress(settings: Settings,
against the kernel rather than against a fixture. The totals are an inequality by design
(`sum(listeners) + sum(endpoints) <= established`): a socket neither config file describes
is counted nowhere, and on this router that is most of the table.
Phase 32 widened **which** routes can be counted — an endpoint the config wrote as an address,
or one the gateway's own resolver published for the name it wrote — and the two assertions near
the end are stated in those terms, with the endpoint identity derived here so the outbound total
is not simply the source's own arithmetic re-added.
"""
target = next((item for item in settings.host_targets if item.name == settings.gateway_host),
None)
@@ -307,11 +338,14 @@ async def test_the_gateway_source_joins_the_live_ingress(settings: Settings,
for route in payload["connections"]:
assert route["in"]["port"] in configured, "a route hangs off no trunk the page draws"
out = route["out"]
assert (route["connections"] is None) is (out["address"] is not True), (
f"{route['id']}: only an endpoint the config wrote as an address can be counted")
if out["address"] is True and isinstance(out["port"], int):
placed = out["address"] is True or bool(out.get("resolved"))
assert (route["connections"] is None) is (not placed), (
f"{route['id']}: only an endpoint whose address is known — written, or resolved by the "
"gateway itself — can carry a count")
key = endpoint_identity(out)
if key is not None:
joinable.add(str(route["metric"]))
endpoints.setdefault((out["host"], out["port"]), route["connections"])
endpoints.setdefault(key, route["connections"])
else:
assert route["metric"] is None, "a route nobody can count publishes no series"
live_out = sum(value for value in endpoints.values() if isinstance(value, int))
@@ -325,8 +359,61 @@ async def test_the_gateway_source_joins_the_live_ingress(settings: Settings,
stored = gateway.samples(payload, 0.0)
assert all(sample.source == gateway.SOURCE for sample in stored)
assert {sample.metric for sample in stored if "out_conns" in sample.metric} <= joinable, (
"only an address endpoint publishes an outbound gauge — a domain-keyed series is the one "
"phase 30 stopped writing, because the figure it carried belonged to the port")
"only an endpoint whose address the join knows publishes an outbound gauge — written as an "
"address, or resolved by the gateway itself (phase 32); an unplaced name publishes nothing")
async def test_a_resolved_target_on_the_live_ingress_is_never_a_dash(
settings: Settings, agent_tls: dict[str, collectors.AgentTls]) -> None:
"""Phase 32 task 03 against the real ingress: `ok` in the resolver means a number on the page.
The operator's complaint was that one line out of thirty-three moved, and the acceptance line for
removing the dash is that it goes because the gateway published the address — not because a
column was hidden. So every route the resolver placed carries a **finite** `connections` (`0`
allowed: resolved and idle is a reading), a non-empty `out.resolved`, the agent's own `via`, the
timestamp of the lookup, and the gauge that goes with it. Skips when the ingress runs an agent
with no `resolver` section yet — which is every host until task 05 re-stages it.
"""
target = next((item for item in settings.host_targets if item.name == settings.gateway_host),
None)
if target is None:
pytest.skip(f"GATEWAY_HOST={settings.gateway_host!r} is not in HOST_TARGETS")
async with httpx.AsyncClient(timeout=settings.gateway_timeout) as client:
try:
snapshot = await collectors.fetch_snapshot(client, target,
timeout=settings.gateway_timeout,
tls=agent_tls.get(target.name))
except collectors.UpstreamError as exc:
pytest.skip(f"{target.name} did not answer: {exc}")
section = snapshot.get("resolver")
if not isinstance(section, dict):
pytest.skip(f"{target.name} publishes no resolver section — agent 1.3.0 is task 05's "
"re-stage; until then every name-written route is a dash by design")
payload = gateway.parse(snapshot, target.name)
if section.get("available") is not True:
pytest.skip(f"{target.name}'s resolver did not answer "
f"(available={section.get('available')!r}): nothing is placed, so nothing "
"is expected to be counted")
placed = [route for route in payload["connections"] if route["out"].get("resolved")]
assert placed, (f"{target.name} published {len(section.get('entries') or [])} resolver "
"entries and the join placed none of them")
for route in placed:
out = route["out"]
# The count lives on the **route** (`connections[]`), not on its `out` endpoint — the same
# shape `test_the_gateway_source_joins_the_live_ingress` reads. This path only ever ran once
# agent 1.3.0 was installed (before that the whole test skipped for want of a `resolver`
# section), which is how a wrong key survived task 03: a KeyError is not a dash.
assert isinstance(route["connections"], int) and not isinstance(route["connections"], bool), (
f"{route['id']}: resolved to {out['resolved']} and the line is still a dash")
assert out["via"] == section.get("via") == "libc", (
"the vantage is the agent's string, copied, never one this module invented")
assert isinstance(out["resolved_ts"], int | float), (
f"{route['id']}: a count obtained through a lookup must say when the lookup happened")
assert route["metric"] is not None, "a counted route always has its gauge"
stored = {sample.metric for sample in gateway.samples(payload, 0.0)}
assert {route["metric"] for route in placed} <= stored, (
"a resolved route's count must reach the store, or its chart stays empty forever")
@pytest.mark.parametrize("engine", ["gufo", "vllm"])