hub v0.104.0: the guest network gets a reader (R-319), and the hub half of the naming (R-295)
gates / gates (push) Successful in 14s
gates / gates (push) Successful in 14s
Four paper debts and one fact given a reader. Hub-only — nothing to bake. A4 — the entry about "the tester's machine" named a risk correctly and labelled it in a way that invited deleting it. Established from the hub's own store: `peti-felhom` is a REAL machine (482 reports, 2026-02-27 → 2026-07-15, a named person's own box) and the 3.6 GB with no key and no backup is real. `david` → `tester-1` is a DIFFERENT record with no host, no escrow and no report, ever — deleted 07:55:49 and re-created 07:56:47 this morning. The prompt's premise conflated the two; the register now says which is which. A1 — R-312/R-313/R-303 recorded as DECIDED with their re-open triggers, and moved out of STATUS's "Waiting on you", which is now empty. A3 — day0-install §C.1 said pushing the installer publishes it. It has not since R-110. Corrected, with the two manifest pins named and an outside-verification command; the one copy that repeated it (a dated audit, true when written) carries a superseded note. A5 — standing rule 5: evidence comes off the machine at the end of the phase that produced it, before any revert. Earned twice in three days on the same box at the same point (R-320). Four homes, plus what to do when it is already gone. R-295 hub half — „Beállító kód" everywhere; „Visszaállító kód" retired. New `reenroll` mail kind so the mail names the page a REBUILT box actually shows („A szerver beállítása"), not the „Elfelejtett jelszó" page it has no login screen to reach. Naming only; the acceptance pin proves the secret is untouched. R-319 — the hub models `guest_net` after 23 days of receiving and discarding it. The signal is `heals_last_hour`, not `state`: a guest the watchdog keeps repairing reads healthy between repairs. `heal_succeeded` decoded too (R-260's lesson). Unknown is never drawn as healthy — three absences, three sentences. No alarm, deliberately. Three red-proofs, mutations asserted applied. Wire-gate checked tags 182 → 190. B1 — the operator's 2026-08-12 dispositions were NOT in the register; they are now. Third allowlist kind for the five ruled "no reader wanted"; `reporting_disabled` reclassified redundant. 8 read · 5 deliberately unread · 1 redundant · 6 still owed. Also filed: R-321 (a deliberately-silent box still alarms stale/down — the checker is age-only, and decoding the flag would not have fixed it), R-322 (the claim guard has never scanned the hub; a hand scan returns zero, so it is a scope gap, not a defect).
This commit is contained in:
@@ -0,0 +1,259 @@
|
||||
package web
|
||||
|
||||
// Guest-network card (R-319) — the first R-264 reader.
|
||||
//
|
||||
// THE FIXTURES BELOW ARE THE REAL WIRE. `liveGuestNetJSON` is the `guest_net` stanza copied verbatim
|
||||
// out of `demo-felhom-8363b5`'s newest row in the live hub's `host_reports` table on 2026-08-13
|
||||
// (agent 0.129.0). Testing against a hand-written shape would have proved only that the parser
|
||||
// matches my own idea of the format — and the defect this whole class comes from (R-260) was exactly
|
||||
// a hub-side struct that did not match what the agent actually sends.
|
||||
//
|
||||
// Every test drives ServeHTTP, so a template gate that never renders is visible here. That is the
|
||||
// seam-wiring rule: handler tests prove nothing about reachability, and this project has shipped four
|
||||
// features whose entry point was never wired.
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// The real stanza: one owned guest, healthy, DHCP, no repairs. `heals_last_hour` and friends are
|
||||
// ABSENT because the agent marks them `omitempty` and this box has never needed a repair — which is
|
||||
// itself the shape scenario A has to survive.
|
||||
const liveGuestNetJSON = `{
|
||||
"host": {"cpu_percent": 4.0, "memory_percent": 30.0, "disk_percent": 20.0},
|
||||
"guest_net": {
|
||||
"checked_at": "2026-08-13T08:15:33Z",
|
||||
"guests": [
|
||||
{"vmid": 9201, "state": "healthy", "mode": "dhcp", "ip": "192.168.0.149",
|
||||
"has_route": true, "dhclient_alive": true, "checked_at": "2026-08-13T08:14:30Z",
|
||||
"message": "address, default route and dhclient all present"}
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
// The same box after the watchdog has had to keep fixing it — the R-54 heal fields populated. This is
|
||||
// the shape the incident of 2026-07-20 would have produced had the watchdog existed then.
|
||||
const repairingGuestNetJSON = `{
|
||||
"host": {"cpu_percent": 4.0, "memory_percent": 30.0, "disk_percent": 20.0},
|
||||
"guest_net": {
|
||||
"checked_at": "2026-08-13T08:15:33Z",
|
||||
"guests": [
|
||||
{"vmid": 9201, "state": "healthy", "mode": "dhcp", "ip": "192.168.0.149",
|
||||
"has_route": true, "dhclient_alive": true, "checked_at": "2026-08-13T08:14:30Z",
|
||||
"healed": true, "heal_succeeded": true, "last_heal_at": "2026-08-13T08:09:12Z",
|
||||
"heals_last_hour": 6, "message": "dhclient was absent; restarted"}
|
||||
]
|
||||
}
|
||||
}`
|
||||
|
||||
// A report from a capable agent with NO guest_net stanza at all — the watchdog switched off, or a
|
||||
// report predating the feature on this box.
|
||||
const silentGuestNetJSON = `{"host": {"cpu_percent": 4.0, "memory_percent": 30.0, "disk_percent": 20.0}}`
|
||||
|
||||
// The stanza arrives, but its contents are not what the hub expects: `guests` is an object where an
|
||||
// array belongs, and `heals_last_hour` is a string. This is what a wire drift or a truncated write
|
||||
// looks like from the hub's side.
|
||||
const malformedGuestNetJSON = `{
|
||||
"host": {"cpu_percent": 4.0},
|
||||
"guest_net": {"checked_at": "2026-08-13T08:15:33Z", "guests": {"vmid": "nine-two-oh-one"}}
|
||||
}`
|
||||
|
||||
// ── A — a machine reporting healthy guest networking, no repairs → shown as healthy ─────────────
|
||||
//
|
||||
// WRONG OUTCOME GUARDED: an empty or alarming state on a machine that is fine. A card that cried
|
||||
// unknown on every healthy box would be switched off within a week, and then the B case below would
|
||||
// never be seen either.
|
||||
func TestGuestNet_A_HealthyRendersHealthy(t *testing.T) {
|
||||
s, st, _ := newRevealServer(t)
|
||||
cookie, _ := newRevealSession(t, s)
|
||||
seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", liveGuestNetJSON, "")
|
||||
|
||||
body := getHostPage(t, s, cookie, "demo-felhom-8363b5")
|
||||
|
||||
if !strings.Contains(body, "Guest network") {
|
||||
t.Fatal("no Guest network card — the reader shows nothing")
|
||||
}
|
||||
if !strings.Contains(body, "192.168.0.149") {
|
||||
t.Error("the guest's address did not reach the page")
|
||||
}
|
||||
if !strings.Contains(body, "badge-ok") {
|
||||
t.Error("a healthy guest must render as healthy, not as an empty or alarming state")
|
||||
}
|
||||
if strings.Contains(body, "needs attention") {
|
||||
t.Error("a machine that is fine is being alarmed on")
|
||||
}
|
||||
// The unknown branches must NOT fire for a box that reported properly.
|
||||
if strings.Contains(body, "does not run the guest-network watchdog") ||
|
||||
strings.Contains(body, "reported no guest-network state") {
|
||||
t.Error("a reporting box rendered one of the unknown sentences")
|
||||
}
|
||||
}
|
||||
|
||||
// ── B — a machine whose watchdog has repaired the guest repeatedly ──────────────────────────────
|
||||
//
|
||||
// WRONG OUTCOME GUARDED: a green tick because the CURRENT state is fine. This is the exact shape of
|
||||
// the failed-disk-drawn-as-a-healthy-empty-disk defect, and it is the reason this card exists at all:
|
||||
// `state` says "healthy" in this fixture, because between repairs it IS.
|
||||
func TestGuestNet_B_RepeatedRepairsAreVisible(t *testing.T) {
|
||||
s, st, _ := newRevealServer(t)
|
||||
cookie, _ := newRevealSession(t, s)
|
||||
seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", repairingGuestNetJSON, "")
|
||||
|
||||
body := getHostPage(t, s, cookie, "demo-felhom-8363b5")
|
||||
|
||||
if !strings.Contains(body, "needs attention") {
|
||||
t.Fatal("a guest repaired 6 times in an hour is reported as fine — the whole point of the card")
|
||||
}
|
||||
if !strings.Contains(body, "had their networking repaired in the last hour") {
|
||||
t.Error("the climbing-repairs sentence is missing")
|
||||
}
|
||||
if !strings.Contains(body, ">6<") {
|
||||
t.Error("the repair COUNT is not rendered; 'some repairs' is not the signal, the number is")
|
||||
}
|
||||
if !strings.Contains(body, "2026-08-13T08:09:12Z") {
|
||||
t.Error("the last repair time is not offered (it is in the title attribute)")
|
||||
}
|
||||
}
|
||||
|
||||
// ── C — a machine that does not report the fact at all → drawn as unknown, never healthy ────────
|
||||
//
|
||||
// WRONG OUTCOME GUARDED: absence read as good news. THE ONE THAT MATTERS. Two distinct absences are
|
||||
// checked because they need different words: an agent that cannot report it, and a capable agent that
|
||||
// said nothing.
|
||||
func TestGuestNet_C_SilentIsUnknownNotHealthy(t *testing.T) {
|
||||
t.Run("capable agent, no stanza", func(t *testing.T) {
|
||||
s, st, _ := newRevealServer(t)
|
||||
cookie, _ := newRevealSession(t, s)
|
||||
seedNetHost(t, st, "demo-hp-bb76ea", "0.129.0", silentGuestNetJSON, "")
|
||||
|
||||
body := getHostPage(t, s, cookie, "demo-hp-bb76ea")
|
||||
|
||||
if !strings.Contains(body, "reported no guest-network state") {
|
||||
t.Fatal("a silent box must SAY it is unknown")
|
||||
}
|
||||
if strings.Contains(body, "badge-ok") && strings.Contains(body, "Guest network") {
|
||||
assertNoHealthyBadgeInGuestNetCard(t, body)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("agent too old to report it", func(t *testing.T) {
|
||||
s, st, _ := newRevealServer(t)
|
||||
cookie, _ := newRevealSession(t, s)
|
||||
// 0.91.0 predates the R-54 watchdog (v0.92.0): the absence is expected AND still unknown.
|
||||
seedNetHost(t, st, "old-agent-box", "0.91.0", silentGuestNetJSON, "")
|
||||
|
||||
body := getHostPage(t, s, cookie, "old-agent-box")
|
||||
|
||||
if !strings.Contains(body, "does not run the guest-network watchdog") {
|
||||
t.Fatal("an old agent's silence must be named as an old agent's silence")
|
||||
}
|
||||
if !strings.Contains(body, "<strong>unknown</strong>, not healthy") {
|
||||
t.Error("the unknown-is-not-healthy sentence is missing")
|
||||
}
|
||||
assertNoHealthyBadgeInGuestNetCard(t, body)
|
||||
})
|
||||
}
|
||||
|
||||
// ── D — the fact arrives malformed → unknown, and the hub does not 500 ──────────────────────────
|
||||
//
|
||||
// WRONG OUTCOME GUARDED: a page that breaks on one machine's bad field. getHostPage fails the test on
|
||||
// any non-200, so surviving the call IS half the assertion.
|
||||
func TestGuestNet_D_MalformedIsUnknownAndDoesNotBreakThePage(t *testing.T) {
|
||||
s, st, _ := newRevealServer(t)
|
||||
cookie, _ := newRevealSession(t, s)
|
||||
seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", malformedGuestNetJSON, "")
|
||||
|
||||
body := getHostPage(t, s, cookie, "demo-felhom-8363b5") // 200 or the test dies here
|
||||
|
||||
if !strings.Contains(body, "reported no guest-network state") {
|
||||
t.Fatal("a malformed stanza must degrade to unknown")
|
||||
}
|
||||
assertNoHealthyBadgeInGuestNetCard(t, body)
|
||||
}
|
||||
|
||||
// A guest whose state string is one the watchdog never asserts (a future value, or a truncated field)
|
||||
// is drawn as unknown rather than falling through to the healthy branch. The switch in
|
||||
// guestNetGuestView.Unknown is an ALLOW-LIST for exactly this reason: an unrecognised value defaults
|
||||
// to unknown, and adding a state to the agent cannot silently paint it green here.
|
||||
func TestGuestNet_UnrecognisedStateIsUnknown(t *testing.T) {
|
||||
s, st, _ := newRevealServer(t)
|
||||
cookie, _ := newRevealSession(t, s)
|
||||
seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", `{
|
||||
"host": {"cpu_percent": 1.0},
|
||||
"guest_net": {"checked_at": "2026-08-13T08:15:33Z",
|
||||
"guests": [{"vmid": 9201, "state": "quantum", "mode": "dhcp", "ip": "10.0.0.5"}]}}`, "")
|
||||
|
||||
body := getHostPage(t, s, cookie, "demo-felhom-8363b5")
|
||||
|
||||
if !strings.Contains(body, "partly unknown") {
|
||||
t.Fatal("an unrecognised guest state must summarise as unknown, not healthy")
|
||||
}
|
||||
}
|
||||
|
||||
// The empty-list case is deliberately NOT the same as silence: the agent's own contract says a stanza
|
||||
// with a fresh checked_at and no guests means "the watchdog ran and found nothing to report", and that
|
||||
// must stay distinguishable from "the watchdog is not wired" — the shape the v0.91.0 inert seam hid
|
||||
// behind.
|
||||
func TestGuestNet_EmptyListIsNotSilence(t *testing.T) {
|
||||
s, st, _ := newRevealServer(t)
|
||||
cookie, _ := newRevealSession(t, s)
|
||||
seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0",
|
||||
`{"host": {"cpu_percent": 1.0}, "guest_net": {"checked_at": "2026-08-13T08:15:33Z"}}`, "")
|
||||
|
||||
body := getHostPage(t, s, cookie, "demo-felhom-8363b5")
|
||||
|
||||
if !strings.Contains(body, "The watchdog ran and has no guest to report") {
|
||||
t.Fatal("an empty guest list must say the watchdog RAN")
|
||||
}
|
||||
if strings.Contains(body, "reported no guest-network state") {
|
||||
t.Error("an empty list was collapsed into silence — the two are different facts")
|
||||
}
|
||||
}
|
||||
|
||||
// assertNoHealthyBadgeInGuestNetCard checks that the Guest network card contains no healthy badge.
|
||||
// It slices the card out of the page rather than searching the whole body, because `badge-ok` occurs
|
||||
// all over a host page (capabilities, storage, WireGuard) and a whole-page search would pass
|
||||
// vacuously — which is the "instrument that can drop results silently" trap.
|
||||
func assertNoHealthyBadgeInGuestNetCard(t *testing.T, body string) {
|
||||
t.Helper()
|
||||
i := strings.Index(body, "Guest network")
|
||||
if i < 0 {
|
||||
t.Fatal("no Guest network card to slice — the assertion would have passed vacuously")
|
||||
}
|
||||
card := body[i:]
|
||||
if j := strings.Index(card, "</section>"); j >= 0 {
|
||||
card = card[:j]
|
||||
} else {
|
||||
t.Fatal("could not find the end of the Guest network card")
|
||||
}
|
||||
if strings.Contains(card, "badge-ok") {
|
||||
t.Errorf("an UNKNOWN guest-network state rendered a healthy badge:\n%s", card)
|
||||
}
|
||||
}
|
||||
|
||||
// A repair that was ATTEMPTED and FAILED is a harder fact than a repair that worked, and the count
|
||||
// alone cannot express it: six successful repairs is a nuisance, six failed ones is a guest that is
|
||||
// down right now. Dropping `heal_succeeded` from the decoder would be R-260 exactly — the OOB decoder
|
||||
// mirrored five of eight fields and the three it dropped included the deciding one.
|
||||
func TestGuestNet_FailedRepairIsDistinctFromFrequentRepair(t *testing.T) {
|
||||
s, st, _ := newRevealServer(t)
|
||||
cookie, _ := newRevealSession(t, s)
|
||||
seedNetHost(t, st, "demo-felhom-8363b5", "0.129.0", `{
|
||||
"host": {"cpu_percent": 1.0},
|
||||
"guest_net": {"checked_at": "2026-08-13T08:15:33Z",
|
||||
"guests": [{"vmid": 9201, "state": "unhealthy", "mode": "dhcp",
|
||||
"has_route": false, "dhclient_alive": false,
|
||||
"healed": true, "heal_succeeded": false, "heals_last_hour": 3,
|
||||
"message": "dhclient restart did not restore the lease"}]}}`, "")
|
||||
|
||||
body := getHostPage(t, s, cookie, "demo-felhom-8363b5")
|
||||
|
||||
if !strings.Contains(body, "repair failed") {
|
||||
t.Fatal("a repair that was attempted and failed is not distinguished from one that worked")
|
||||
}
|
||||
if !strings.Contains(body, "needs attention") {
|
||||
t.Error("a guest whose repair failed must summarise as needing attention")
|
||||
}
|
||||
assertNoHealthyBadgeInGuestNetCard(t, body)
|
||||
}
|
||||
Reference in New Issue
Block a user