R-444: hub half — the System page shows each box's last disk trim
sysfacts reads the agent's top-level guest_disk_trim stanza (schedule + per-guest last attempt: vmid, last_attempt_at, ok, bytes_trimmed, mounts, duration_seconds, last_ok_at, error) into a field-by-field mirror. The System page's new 'Last disk trim' column shows the last successful trim and the GiB it freed; amber when the newest attempt failed (error shown) or last_ok_at is older than 14 days (judged on the success time, never the attempt time); '—' when the agent sends no stanza. wire_contract_gate: SUBTREE_MIRRORS checks guest_disk_trim field by field BOTH ways against sysfacts.DiskTrim; decoys (ok renamed, last_ok_at dropped) in test_gate_decoys.py. Decision 139. Co-Authored-By: Claude Opus 5.5 (1M context) <noreply@anthropic.com> Claude-Session: https://claude.ai/code/session_0159rPz1ZhFKsS53msqPYxtS
This commit is contained in:
@@ -59,6 +59,28 @@ type ConfigBundle struct {
|
||||
Drift []string `json:"drift"`
|
||||
}
|
||||
|
||||
// DiskTrim is the agent's weekly guest disk trim stanza (R-444, `09` §3 decision 139): the host report's TOP-LEVEL
|
||||
// `guest_disk_trim` object (agent internal/hub GuestDiskTrimStatus). A field-by-field mirror of the agent's type,
|
||||
// checked BOTH ways by the wire-contract gate (SUBTREE_MIRRORS in scripts/wire_contract_gate.py), so a rename or a new
|
||||
// field on either side convicts.
|
||||
type DiskTrim struct {
|
||||
Schedule string `json:"schedule"`
|
||||
Guests []GuestTrim `json:"guests"`
|
||||
}
|
||||
|
||||
// GuestTrim is one guest's LAST trim attempt. `ok` with `last_attempt_at` is that attempt's verdict; `last_ok_at` is
|
||||
// the last attempt that succeeded ("" = never) — staleness is judged on it, never on the attempt time alone.
|
||||
type GuestTrim struct {
|
||||
VMID int `json:"vmid"`
|
||||
LastAttemptAt string `json:"last_attempt_at"` // RFC3339
|
||||
OK bool `json:"ok"`
|
||||
BytesTrimmed int64 `json:"bytes_trimmed"`
|
||||
Mounts int `json:"mounts"`
|
||||
DurationSeconds float64 `json:"duration_seconds"`
|
||||
LastOKAt string `json:"last_ok_at"` // RFC3339, "" = never
|
||||
Error string `json:"error"`
|
||||
}
|
||||
|
||||
// System is the whole stanza. Present is false for a report from an agent older than v0.142.0.
|
||||
type System struct {
|
||||
Present bool
|
||||
@@ -70,10 +92,14 @@ type System struct {
|
||||
Host Host
|
||||
Guest Guest
|
||||
Bundle ConfigBundle
|
||||
// DiskTrim is the top-level `guest_disk_trim` stanza (R-444). Independent of the `system` stanza: it is read even
|
||||
// when Present is false. nil = the agent sent none (an agent with no trimmer).
|
||||
DiskTrim *DiskTrim
|
||||
}
|
||||
|
||||
type wire struct {
|
||||
System *struct {
|
||||
DiskTrim *DiskTrim `json:"guest_disk_trim"`
|
||||
System *struct {
|
||||
PVEVersion string `json:"pve_version"`
|
||||
KernelVersion string `json:"kernel_version"`
|
||||
VMID int `json:"vmid"`
|
||||
@@ -97,7 +123,11 @@ func Parse(reportJSON string) System {
|
||||
Host: Host{Debian: Unknown, KernelRunning: Unknown, KernelNextBoot: Unknown},
|
||||
Guest: Guest{Debian: Unknown, DockerEngine: Unknown, Containerd: Unknown, LiveRestore: Unknown}}
|
||||
var w wire
|
||||
if json.Unmarshal([]byte(reportJSON), &w) != nil || w.System == nil {
|
||||
if json.Unmarshal([]byte(reportJSON), &w) != nil {
|
||||
return out
|
||||
}
|
||||
out.DiskTrim = w.DiskTrim
|
||||
if w.System == nil {
|
||||
return out
|
||||
}
|
||||
out.Present = true
|
||||
|
||||
@@ -41,3 +41,32 @@ func TestParse_AbsentAndPartialAreUnknown(t *testing.T) {
|
||||
t.Fatal("unknown kernels must not read as 'differs'")
|
||||
}
|
||||
}
|
||||
|
||||
// R-444: the top-level `guest_disk_trim` stanza is read on its own — with or without a `system` stanza — and an absent
|
||||
// stanza stays nil (the page says "—", never a made-up trim). The fixture is the agent's TestReportJSONShape output shape.
|
||||
func TestParse_DiskTrim(t *testing.T) {
|
||||
s := Parse(`{"host":{},"guest_disk_trim":{"schedule":"weekly, Wednesday from 10:00","guests":[
|
||||
{"vmid":9201,"last_attempt_at":"2026-10-07T08:30:00Z","ok":true,"bytes_trimmed":90143313920,"mounts":2,"duration_seconds":24.4,"last_ok_at":"2026-10-07T08:30:00Z"},
|
||||
{"vmid":9202,"last_attempt_at":"2026-10-07T08:35:00Z","ok":false,"bytes_trimmed":0,"mounts":0,"duration_seconds":1.2,"error":"pct fstrim: exit 255"}]}}`)
|
||||
if s.Present {
|
||||
t.Error("no system stanza: Present must stay false")
|
||||
}
|
||||
d := s.DiskTrim
|
||||
if d == nil || d.Schedule != "weekly, Wednesday from 10:00" || len(d.Guests) != 2 {
|
||||
t.Fatalf("guest_disk_trim misread: %+v", d)
|
||||
}
|
||||
g, f := d.Guests[0], d.Guests[1]
|
||||
if g.VMID != 9201 || !g.OK || g.BytesTrimmed != 90143313920 || g.Mounts != 2 || g.DurationSeconds != 24.4 ||
|
||||
g.LastAttemptAt != "2026-10-07T08:30:00Z" || g.LastOKAt != "2026-10-07T08:30:00Z" {
|
||||
t.Errorf("guest 9201 misread: %+v", g)
|
||||
}
|
||||
if f.OK || f.Error != "pct fstrim: exit 255" || f.LastOKAt != "" {
|
||||
t.Errorf("guest 9202 misread: %+v", f)
|
||||
}
|
||||
if s = Parse(full); s.DiskTrim != nil {
|
||||
t.Errorf("a report with no guest_disk_trim must read nil, got %+v", s.DiskTrim)
|
||||
}
|
||||
if s = Parse(`{"system":{"pve_version":"x"},"guest_disk_trim":{"schedule":"w","guests":[]}}`); !s.Present || s.DiskTrim == nil || len(s.DiskTrim.Guests) != 0 {
|
||||
t.Errorf("with a system stanza the trim must still be read: %+v", s)
|
||||
}
|
||||
}
|
||||
|
||||
@@ -40,6 +40,7 @@ type systemRow struct {
|
||||
Agent cell // R-530: the box's agent against the vouched one
|
||||
// guest
|
||||
GuestDebian, GuestRelease, GuestPending, GuestRestart cell
|
||||
Trim cell // R-444: the last `pct fstrim` of the guest
|
||||
// docker
|
||||
Engine, Containerd, LiveRestore, DockerRelease cell
|
||||
// last leg
|
||||
@@ -112,6 +113,77 @@ func buildFloorRows(ovs []store.CustomerFloorOverride, global string, now time.T
|
||||
|
||||
func plain(s string) cell { return cell{Text: s} }
|
||||
|
||||
// trimStaleAfter is when a guest's last SUCCESSFUL disk trim reads amber: the boxes trim weekly (`09` §3 decision
|
||||
// 139), so two missed weeks are worth a look.
|
||||
const trimStaleAfter = 14 * 24 * time.Hour
|
||||
|
||||
// trimCell is the "Last disk trim" cell (R-444): per guest, the last successful `pct fstrim` (from `last_ok_at`) and what
|
||||
// the newest attempt freed. Amber when the newest attempt failed (its error is shown) or the last success is older than
|
||||
// 14 days — judged on `last_ok_at`, never on the attempt time (an attempt is not a result). "—" when the agent sends no
|
||||
// stanza (no trimmer); an unreadable time is "unknown", never a guess.
|
||||
func trimCell(dt *sysfacts.DiskTrim, now time.Time) cell {
|
||||
if dt == nil {
|
||||
return cell{Text: "—", Title: "the agent reports no disk trim (an agent without the weekly trim)"}
|
||||
}
|
||||
if len(dt.Guests) == 0 {
|
||||
return cell{Text: "none yet", Title: "the weekly trim runs (" + dt.Schedule + ") and has not trimmed a guest yet"}
|
||||
}
|
||||
out := cell{Title: "schedule: " + dt.Schedule}
|
||||
var texts []string
|
||||
for _, g := range dt.Guests {
|
||||
t, warn, why := guestTrimText(g, now)
|
||||
if len(dt.Guests) > 1 {
|
||||
t = fmt.Sprintf("%d: %s", g.VMID, t)
|
||||
}
|
||||
texts = append(texts, t)
|
||||
if warn {
|
||||
out.Class = "warn"
|
||||
out.Title = fmt.Sprintf("guest %d: %s — %s", g.VMID, why, out.Title)
|
||||
}
|
||||
}
|
||||
out.Text = strings.Join(texts, " / ")
|
||||
return out
|
||||
}
|
||||
|
||||
func guestTrimText(g sysfacts.GuestTrim, now time.Time) (text string, warn bool, why string) {
|
||||
var okAt time.Time
|
||||
if g.LastOKAt != "" {
|
||||
t, err := time.Parse(time.RFC3339, g.LastOKAt)
|
||||
if err != nil {
|
||||
return "unknown", true, fmt.Sprintf("unreadable last_ok_at %q", g.LastOKAt)
|
||||
}
|
||||
okAt = t
|
||||
}
|
||||
attAt, aerr := time.Parse(time.RFC3339, g.LastAttemptAt)
|
||||
if aerr != nil {
|
||||
return "unknown", true, fmt.Sprintf("unreadable last_attempt_at %q", g.LastAttemptAt)
|
||||
}
|
||||
if g.OK {
|
||||
text = fmt.Sprintf("%s · %.1f GiB", ago(attAt, now), float64(g.BytesTrimmed)/(1<<30))
|
||||
if !okAt.IsZero() {
|
||||
text = fmt.Sprintf("%s · %.1f GiB", ago(okAt, now), float64(g.BytesTrimmed)/(1<<30))
|
||||
} else {
|
||||
okAt = attAt
|
||||
}
|
||||
} else {
|
||||
last := "never ok"
|
||||
if !okAt.IsZero() {
|
||||
last = "last ok " + ago(okAt, now)
|
||||
}
|
||||
errText := g.Error
|
||||
if errText == "" {
|
||||
errText = "no error text"
|
||||
}
|
||||
text = fmt.Sprintf("FAILED %s: %s (%s)", ago(attAt, now), errText, last)
|
||||
warn, why = true, "the newest trim failed"
|
||||
}
|
||||
if !okAt.IsZero() && now.Sub(okAt) > trimStaleAfter {
|
||||
warn = true
|
||||
why = strings.TrimPrefix(why+"; ", "; ") + "the last successful trim is older than 14 days (the boxes trim weekly)"
|
||||
}
|
||||
return text, warn, why
|
||||
}
|
||||
|
||||
// bundleCell is the "Root files" cell (R-840): the box's config bundle against the vouched agent's. Amber while behind,
|
||||
// red from the alarm's wait on, amber when a file was changed by hand (drift); "unknown" is never coloured as a fact.
|
||||
func bundleCell(f sysfacts.System, vouchedAgent, vouchedSHA string, since time.Time, after time.Duration, now time.Time) cell {
|
||||
@@ -239,6 +311,7 @@ func buildSystemRows(lines []osupdates.FleetLine, facts map[string]sysfacts.Syst
|
||||
r.GuestRelease = plain(orDash(l.Guest.ReleaseID))
|
||||
r.GuestPending = plain(fmt.Sprint(l.Guest.Pending))
|
||||
r.GuestRestart = plain(fmt.Sprint(l.Guest.RestartNeeded))
|
||||
r.Trim = trimCell(f.DiskTrim, now)
|
||||
r.Engine, r.Containerd = unknownCell(f.Guest.DockerEngine), unknownCell(f.Guest.Containerd)
|
||||
r.LiveRestore = unknownCell(f.Guest.LiveRestore)
|
||||
if f.Guest.LiveRestore == "off" {
|
||||
@@ -312,16 +385,16 @@ func (s *Server) handleSystem(w http.ResponseWriter, r *http.Request) {
|
||||
s.logger.Printf("[ERROR] system page: floor overrides: %v", oerr)
|
||||
}
|
||||
data := map[string]interface{}{
|
||||
"Rows": rows,
|
||||
"Rows": rows,
|
||||
"GlobalFloor": global,
|
||||
"VouchedAgent": man.AgentVersion,
|
||||
"Floors": buildFloorRows(ovs, global, time.Now()),
|
||||
"Releases": view.Releases(),
|
||||
"Cancelled": view.CancelledReleases(),
|
||||
"Candidates": view.Candidates(),
|
||||
"Flash": r.URL.Query().Get("flash"),
|
||||
"FlashErr": r.URL.Query().Get("err"),
|
||||
"CSRFToken": s.getCSRFToken(r),
|
||||
"Releases": view.Releases(),
|
||||
"Cancelled": view.CancelledReleases(),
|
||||
"Candidates": view.Candidates(),
|
||||
"Flash": r.URL.Query().Get("flash"),
|
||||
"FlashErr": r.URL.Query().Get("err"),
|
||||
"CSRFToken": s.getCSRFToken(r),
|
||||
}
|
||||
if err := s.templates.ExecuteTemplate(w, "system.html", data); err != nil {
|
||||
s.logger.Printf("[ERROR] system.html template: %v", err)
|
||||
|
||||
@@ -0,0 +1,81 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"fmt"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/sysfacts"
|
||||
)
|
||||
|
||||
// R-444 (`09` §3 decision 139): the System page's "Last disk trim" cell — one case per branch. Staleness is judged on
|
||||
// last_ok_at, never on the attempt time ("presence is not success").
|
||||
func TestTrimCell(t *testing.T) {
|
||||
now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
|
||||
at := func(d time.Duration) string { return now.Add(-d).Format(time.RFC3339) }
|
||||
day := 24 * time.Hour
|
||||
dt := func(gs ...sysfacts.GuestTrim) *sysfacts.DiskTrim {
|
||||
return &sysfacts.DiskTrim{Schedule: "weekly", Guests: gs}
|
||||
}
|
||||
cases := []struct {
|
||||
name string
|
||||
dt *sysfacts.DiskTrim
|
||||
text string
|
||||
class string
|
||||
titleHas string
|
||||
}{
|
||||
{"agent sends nothing", nil, "—", "", "reports no disk trim"},
|
||||
{"stanza, no guest yet", dt(), "none yet", "", "has not trimmed a guest yet"},
|
||||
{"recent ok", dt(sysfacts.GuestTrim{VMID: 9201, LastAttemptAt: at(2 * day), LastOKAt: at(2 * day), OK: true, BytesTrimmed: 32427003904}),
|
||||
"2 days ago · 30.2 GiB", "", "schedule: weekly"},
|
||||
{"ok but older than 14 days", dt(sysfacts.GuestTrim{VMID: 9201, LastAttemptAt: at(15 * day), LastOKAt: at(15 * day), OK: true, BytesTrimmed: 1 << 30}),
|
||||
"15 days ago · 1.0 GiB", "warn", "older than 14 days"},
|
||||
{"exactly 14 days is not stale", dt(sysfacts.GuestTrim{VMID: 9201, LastAttemptAt: at(14 * day), LastOKAt: at(14 * day), OK: true}),
|
||||
"14 days ago · 0.0 GiB", "", ""},
|
||||
{"newest failed, recent success before it", dt(sysfacts.GuestTrim{VMID: 9201, LastAttemptAt: at(1 * day), LastOKAt: at(8 * day), OK: false, Error: "pct fstrim: exit 255"}),
|
||||
"FAILED 24 h ago: pct fstrim: exit 255 (last ok 8 days ago)", "warn", "the newest trim failed"},
|
||||
{"failed and never ok", dt(sysfacts.GuestTrim{VMID: 9201, LastAttemptAt: at(1 * day), OK: false}),
|
||||
"FAILED 24 h ago: no error text (never ok)", "warn", "the newest trim failed"},
|
||||
{"a recent ATTEMPT does not hide a 20-day-old success", dt(sysfacts.GuestTrim{VMID: 9201, LastAttemptAt: at(1 * day), LastOKAt: at(20 * day), OK: false, Error: "busy"}),
|
||||
"FAILED 24 h ago: busy (last ok 20 days ago)", "warn", "older than 14 days"},
|
||||
{"two guests, one amber", dt(
|
||||
sysfacts.GuestTrim{VMID: 9201, LastAttemptAt: at(2 * day), LastOKAt: at(2 * day), OK: true, BytesTrimmed: 2 << 30},
|
||||
sysfacts.GuestTrim{VMID: 9202, LastAttemptAt: at(1 * day), OK: false, Error: "x"}),
|
||||
"9201: 2 days ago · 2.0 GiB / 9202: FAILED 24 h ago: x (never ok)", "warn", "guest 9202"},
|
||||
{"unreadable time", dt(sysfacts.GuestTrim{VMID: 9201, LastAttemptAt: "yesterday", OK: true}), "unknown", "warn", "unreadable last_attempt_at"},
|
||||
}
|
||||
for _, c := range cases {
|
||||
got := trimCell(c.dt, now)
|
||||
if got.Text != c.text || got.Class != c.class || !strings.Contains(got.Title, c.titleHas) {
|
||||
t.Errorf("%s: got %+v, want text %q class %q title containing %q", c.name, got, c.text, c.class, c.titleHas)
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
// The cell reaches the rendered page, read from the stored host report (the endpoint the operator's browser calls).
|
||||
func TestSystemPage_LastDiskTrim(t *testing.T) {
|
||||
s, st, _ := systemServer(t)
|
||||
when := time.Now().Add(-20 * 24 * time.Hour).UTC().Format(time.RFC3339)
|
||||
body := fmt.Sprintf(`{"host":{"cpu_percent":1},"guest_disk_trim":{"schedule":"weekly","guests":[{"vmid":9201,"last_attempt_at":%q,"ok":true,"bytes_trimmed":32427003904,"mounts":2,"duration_seconds":24.4,"last_ok_at":%q}]}}`, when, when)
|
||||
if err := st.UpsertHost(&store.Host{HostID: "trim-1", CustomerID: "c-trim", APIKey: "k-trim"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if err := st.SaveHostReport("trim-1", "c-trim", []byte(body), store.HostReportDenorm{AgentVersion: "0.150.0", CloudflaredStatus: "running"}); err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
b := getSystem(t, s)
|
||||
if !strings.Contains(b, ">Last disk trim</th>") {
|
||||
t.Error("the System page lacks the Last disk trim column")
|
||||
}
|
||||
if !strings.Contains(b, "20 days ago · 30.2 GiB") {
|
||||
t.Errorf("the trim-1 box's trim is not on the page")
|
||||
}
|
||||
if !strings.Contains(b, `class="c-warn" title="guest 9201: the last successful trim is older than 14 days`) {
|
||||
t.Error("a 20-day-old trim must be amber, with its reason")
|
||||
}
|
||||
if !strings.Contains(b, `title="the agent reports no disk trim`) {
|
||||
t.Error("a box whose agent sends no trim must read — with its reason")
|
||||
}
|
||||
}
|
||||
@@ -104,11 +104,11 @@
|
||||
<tr>
|
||||
<th>Box</th><th>Ring / updates</th><th>Tunnel</th>
|
||||
<th class="grp">Proxmox</th><th>Kernel (running)</th><th>Kernel (next boot)</th><th>Debian</th><th>Felhom release</th><th>Pending</th><th>Not covered</th><th>Held</th><th>Reboot needed</th><th>kernel.panic</th><th>Oops</th><th>Crash restarts 24 h</th><th>Crash guard</th><th title="The root-owned config bundle: sudoers, wrappers, units (R-840)">Root files</th><th title="The box's agent against the vouched one (R-530). Agents update only by a per-box signed job.">Agent</th>
|
||||
<th class="grp">Guest Debian</th><th>Felhom release</th><th>Pending</th><th>Restart needed</th>
|
||||
<th class="grp">Guest Debian</th><th>Felhom release</th><th>Pending</th><th>Restart needed</th><th title="The newest pct fstrim of the guest's disks, and what it freed (R-444). The boxes trim weekly.">Last disk trim</th>
|
||||
<th class="grp">Docker</th><th>containerd</th><th>live-restore</th><th>Docker release</th>
|
||||
<th class="grp">Last OS leg</th>
|
||||
</tr>
|
||||
<tr class="text-muted"><th></th><th></th><th></th><th class="grp" colspan="15">host</th><th class="grp" colspan="4">guest</th><th class="grp" colspan="4">Docker engine</th><th class="grp"></th></tr>
|
||||
<tr class="text-muted"><th></th><th></th><th></th><th class="grp" colspan="15">host</th><th class="grp" colspan="5">guest</th><th class="grp" colspan="4">Docker engine</th><th class="grp"></th></tr>
|
||||
</thead>
|
||||
<tbody>
|
||||
{{range .Rows}}
|
||||
@@ -136,7 +136,7 @@
|
||||
{{template "sys_cell" .Held}}{{template "sys_cell" .RebootSince}}{{template "sys_cell" .KernelPanic}}{{template "sys_cell" .Oops}}
|
||||
{{template "sys_cell" .CrashRestarts24h}}{{template "sys_cell" .Guard}}{{template "sys_cell" .Bundle}}{{template "sys_cell" .Agent}}
|
||||
<td class="grp {{if .GuestDebian.Class}}c-{{.GuestDebian.Class}}{{end}}">{{.GuestDebian.Text}}</td>
|
||||
{{template "sys_cell" .GuestRelease}}{{template "sys_cell" .GuestPending}}{{template "sys_cell" .GuestRestart}}
|
||||
{{template "sys_cell" .GuestRelease}}{{template "sys_cell" .GuestPending}}{{template "sys_cell" .GuestRestart}}{{template "sys_cell" .Trim}}
|
||||
<td class="grp {{if .Engine.Class}}c-{{.Engine.Class}}{{end}}">{{.Engine.Text}}</td>
|
||||
{{template "sys_cell" .Containerd}}{{template "sys_cell" .LiveRestore}}{{template "sys_cell" .DockerRelease}}
|
||||
<td class="grp {{if .LastLeg.Class}}c-{{.LastLeg.Class}}{{end}}" title="{{.LastLeg.Title}}">{{.LastLeg.Text}}</td>
|
||||
|
||||
@@ -63,7 +63,9 @@ COVERS = {
|
||||
"must convict (it passed for months as `language` did); the genuine article — the same "
|
||||
"name in a struct tag beside a `//` inside a string literal — must pass; and R-315: the "
|
||||
"agent RENAMING its mirror's `superseded_at` tag (the old name still occurs as a local-API "
|
||||
"map key) must convict on the field-by-field mirror check"),
|
||||
"map key) must convict on the field-by-field mirror check; and R-444: the agent renaming "
|
||||
"`guest_disk_trim.guests.ok` to `succeeded` (a name the hub carries elsewhere) or DROPPING `last_ok_at` (a field the "
|
||||
"hub reads and would show as never-ok for ever) must convict on the subtree mirror"),
|
||||
"stands": ("R-819: a stand citing a register id that exists in NEITHER register, and a stand "
|
||||
"marked 'walked' whose only source is a register row (a green dot from a label); "
|
||||
"plus the genuine article — a stand citing only a CLOSED row, which must PASS"),
|
||||
@@ -750,6 +752,47 @@ for _mode, _want in (("renamed", 10), ("genuine", 0)):
|
||||
print(" ok %-20s %s" % ("wire-mirror/" + _mode,
|
||||
"decoy rejected" if _want else "genuine accepted"))
|
||||
|
||||
# ── wire-contract subtree mirror (R-444) ─────────────────────────────────────────────────────────
|
||||
#
|
||||
# `guest_disk_trim` is checked field by field BOTH ways against hub sysfacts.DiskTrim. Two decoys on the agent's parsed
|
||||
# GuestDiskTrim body (no tree copy): `ok` renamed to `succeeded` — the name check passes it, since `succeeded` occurs in the hub — and
|
||||
# `last_ok_at` dropped, which only the reverse direction can see. The genuine article must pass.
|
||||
_WCS = r"""
|
||||
import os, sys
|
||||
sys.path.insert(0, "scripts")
|
||||
import wire_contract_gate as g
|
||||
mode = sys.argv[1]
|
||||
orig, agent = g.build_index, os.path.abspath(g.REPOS["agent"])
|
||||
def patched(root):
|
||||
by_dir, by_name = orig(root)
|
||||
if mode != "genuine" and os.path.abspath(root) == agent:
|
||||
k = ("internal/hub", "GuestDiskTrim")
|
||||
b = by_dir[k]
|
||||
if mode == "renamed":
|
||||
assert 'json:"ok"' in b, "mutation target missing"
|
||||
b = b.replace('json:"ok"', 'json:"succeeded"')
|
||||
else:
|
||||
assert 'json:"last_ok_at' in b, "mutation target missing"
|
||||
b = "\n".join(l for l in b.split("\n") if 'json:"last_ok_at' not in l)
|
||||
by_dir[k] = b
|
||||
by_name["GuestDiskTrim"] = [(d, b if d == k[0] else x) for d, x in by_name["GuestDiskTrim"]]
|
||||
return by_dir, by_name
|
||||
g.build_index = patched
|
||||
rc, conv = g.run(quiet=True)
|
||||
want = {"renamed": "guest_disk_trim.guests.succeeded", "dropped": "guest_disk_trim.guests.last_ok_at"}.get(mode)
|
||||
hit = any(d == want for _, _, m in conv for _, d in m)
|
||||
print("rc=%d convicted=%s" % (rc, hit))
|
||||
sys.exit(0 if rc == 0 else (10 if hit else 11))
|
||||
"""
|
||||
for _mode, _want in (("renamed", 10), ("dropped", 10), ("genuine", 0)):
|
||||
ran += 1
|
||||
_p = subprocess.run([sys.executable, "-c", _WCS, _mode], cwd=ROOT, capture_output=True, text=True)
|
||||
if _p.returncode != _want:
|
||||
fails.append("wire-trim/%s: rc=%d, want %d (10 = the mutated trim field convicted, 0 = passed)\n%s"
|
||||
% (_mode, _p.returncode, _want, (_p.stdout + _p.stderr)[-500:]))
|
||||
else:
|
||||
print(" ok %-20s %s" % ("wire-trim/" + _mode, "decoy rejected" if _want else "genuine accepted"))
|
||||
|
||||
# ── stands (R-819) ───────────────────────────────────────────────────────────────────────────────
|
||||
#
|
||||
# check_stands.py was red and in no runner. Its decoys are stand files written as a session would write
|
||||
|
||||
@@ -105,6 +105,17 @@ MIRRORS = {
|
||||
"hub -> controller (report ACK, `escrow` object)": ("internal/report", "EscrowStatus"),
|
||||
}
|
||||
|
||||
# R-444 (2026-10-06): a SUBTREE of a name-checked root whose receiver decodes it into one named mirror type gets the
|
||||
# field-by-field check in BOTH directions — every emitted path under it must be a json path of the mirror, AND every
|
||||
# mirror path must be emitted (a field the hub reads and the agent never sends would leave the page on "—" for ever).
|
||||
# Both directions, because some leaves here are short names (`ok`, `error`, `vmid`) the name check cannot judge:
|
||||
# `ok` occurs everywhere, so a renamed `ok` would pass it. Keyed by (root label, dotted subtree path):
|
||||
# (receiver package dir, receiver type). A subtree the emitter does not carry YET prints PENDING — the receiver was
|
||||
# built first, against a provisional shape — and is neither a pass nor a conviction of the subtree.
|
||||
SUBTREE_MIRRORS = {
|
||||
("agent -> hub (POST /host-report)", "guest_disk_trim"): ("internal/sysfacts", "DiskTrim"),
|
||||
}
|
||||
|
||||
# Tag names whose literal string carries no information in a repo-wide search. NOT CHECKED.
|
||||
# Listed rather than silently skipped: each one is a hole.
|
||||
GENERIC = {
|
||||
@@ -630,6 +641,29 @@ def run(root_override=None, quiet=False):
|
||||
convictions.append((label, receiver, missing))
|
||||
continue
|
||||
kinds.append((label, "name-reachability only (a tag found ANYWHERE in %s passes)" % receiver))
|
||||
emitted_paths = {d for _, d in tags}
|
||||
for (slabel, sub), (mdir, mtype) in sorted(SUBTREE_MIRRORS.items()):
|
||||
if slabel != label:
|
||||
continue
|
||||
rby_dir, rby_name = indexes[receiver]
|
||||
if (mdir, mtype) not in rby_dir:
|
||||
die("wire-contract gate INCONCLUSIVE: declared subtree mirror %s.%s not found in %s/%s\n"
|
||||
" A mirror that cannot be resolved is not a pass — fix SUBTREE_MIRRORS or the type."
|
||||
% (receiver, mtype, receiver, mdir))
|
||||
if sub not in emitted_paths:
|
||||
kinds.append((" └ " + sub, "PENDING — the emitter does not carry `%s` yet; %s.%s is unchecked"
|
||||
% (sub, mdir, mtype)))
|
||||
continue
|
||||
want = {sub + "." + d for _, d in walk(rby_dir, rby_name, mdir, mtype)}
|
||||
got = {d for d in emitted_paths if d.startswith(sub + ".")}
|
||||
for d in sorted(got - want):
|
||||
checked += 1
|
||||
missing.append((d.split(".")[-1], d))
|
||||
for d in sorted(want - got):
|
||||
checked += 1
|
||||
missing.append((d.split(".")[-1] + " (read by the receiver, never emitted)", d))
|
||||
kinds.append((" └ " + sub, "FIELD-BY-FIELD both ways against %s %s.%s (%d path(s))"
|
||||
% (receiver, mdir, mtype, len(want | got))))
|
||||
for tag, dotted in sorted(seen_tags.items()):
|
||||
if tag in GENERIC:
|
||||
skipped += 1
|
||||
|
||||
Reference in New Issue
Block a user