Both are one shape: something the box already knows, drawn as its opposite. R-259 — A DISK WE FAILED TO READ WAS DRAWN AS A HEALTHY EMPTY DISK. readDiskUsage (internal/system/info_linux.go) logged a statfs failure at DEBUG and returned, leaving the caller's TotalGB/UsedGB/AvailGB/Percent at zero — and usageColor(0) is "nominal". The dashboard's most-looked-at meter therefore rendered "0.0 GB / 0.0 GB (0%)" with a 0%-wide bar in the healthy colour. "We could not look" and "there is plenty of room" were the same picture. readDiskUsage now returns whether the measurement succeeded; SystemInfo gains DiskKnown and HDDKnown (HDDConfigured is not a substitute: it says a path was configured, not that reading it worked); and the template draws NO figure, NO percentage and NO meter fill when unknown, saying "A tarhely merete most nem olvashato ki." instead. A healthy box is byte-identical, colour band included. This session rules the convention (felhom.eu CONTEXT.md S-39): an explicit `...Known bool` companion beside the figures, checked in the template — the shape Offbox.StatsKnown already uses, whose own comment says "a 0%-wide bar over an unread store is a picture of emptiness, and a picture is a claim". Pointers and separate error fields are both legitimate Go, but a codebase with three dialects cannot be gated (ROADMAP G-3 was blocked on exactly this). Existing call sites NOT converted. R-258 — THE PER-APP BACKUP TICK WAS GREEN ON PRESENCE, AND RED ONLY ON A GLOBAL CONDITION. buildAppBackupRows set Tier1LastStatus from status.LastDBDump.Success, which is the box's single most recent dump RUN, whichever app it belonged to. An app whose own dump failed showed a tick as long as some other app dumped successfully afterwards; an app with no database took the nil branch and went green on the mere existence of a restore point. appDumpVerdict now reads THIS app's own entries in DBDumpStatus.Results (matched on DumpResult.DB.StackName, failure = non-nil Error). Three states: any failing database -> error; all clean -> ok; no result recorded -> NO verdict and no icon, titled "Errol a mentesrol nincs eredmenyunk." The recovery unit carries no per-run outcome of its own, so green cannot honestly be derived from presence. The global tier1DBStatus label is untouched — it is correct as a global. RECENCY IS DELIBERATELY NOT ADDED. A tick over a three-week-old restore point is a real weakness, but an age threshold means inventing a number and the time is already printed beside the icon. Recorded as an observation, not changed. AN EXISTING TEST WAS ASSERTING THE DEFECT AND WAS CORRECTED, NOT DELETED: TestBuildAppBackupRows_Tier1FromRestorePoints expected "ok" for a status with no LastDBDump at all — green from nothing but a file's existence. It now expects no verdict; its real subject, the Tier1LastRun time, is unchanged. The dashboard test EXTRACTS the meter block from the shipped template rather than copying it: a copied block drifts, and a drifted copy passes while the page it claims to cover has changed — the fixture-is-not-the-wire mistake this project has now hit twice. Six red-proofs across both parts, each with the mutation asserted applied. No new tag on any declared wire — report/builder.go maps into its own types and is untouched; wire_contract_gate.py confirmed green. go build / go vet / go test ./... green (28 packages), controller_gates --fast all OK, both run separately from this commit.
This commit is contained in:
@@ -0,0 +1,83 @@
|
||||
package web
|
||||
|
||||
import (
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/appbackup"
|
||||
"gitea.dooplex.hu/admin/felhom-controller/internal/backup"
|
||||
)
|
||||
|
||||
// R-258 — the per-app tier-1 tick must answer about THIS app, and say nothing when it knows nothing.
|
||||
//
|
||||
// Driven through appDumpVerdict, which is the seam buildAppBackupRows now calls. The defect was a
|
||||
// verdict derived from a GLOBAL field, so the test that matters is the one with two apps where the
|
||||
// global answer and the per-app answer disagree.
|
||||
|
||||
func res(stack string, err error) appbackup.DumpResult {
|
||||
return appbackup.DumpResult{DB: appbackup.DiscoveredDB{StackName: stack}, Error: err}
|
||||
}
|
||||
|
||||
// SCENARIO F — X's own dump failed; Y's succeeded and is the most recent on the box.
|
||||
func TestAppDumpVerdict_IsPerApp_NotTheBoxsMostRecentRun(t *testing.T) {
|
||||
dump := &backup.DBDumpStatus{
|
||||
LastRun: time.Now(),
|
||||
// Y ran last and succeeded, so the box-level Success is true — which is exactly the value
|
||||
// the old code used for every app.
|
||||
Success: true,
|
||||
Results: []appbackup.DumpResult{
|
||||
res("appX", errors.New("pg_dump: connection refused")),
|
||||
res("appY", nil),
|
||||
},
|
||||
}
|
||||
|
||||
if got := appDumpVerdict(dump, "appX"); got != "error" {
|
||||
t.Errorf("app X's own dump FAILED but its tick is %q, want \"error\".\n"+
|
||||
"This is R-258: the verdict was read from the box's most recent dump run — app Y's — so a "+
|
||||
"failed backup showed a green tick to the customer.", got)
|
||||
}
|
||||
if got := appDumpVerdict(dump, "appY"); got != "ok" {
|
||||
t.Errorf("app Y succeeded but its tick is %q, want \"ok\"", got)
|
||||
}
|
||||
}
|
||||
|
||||
// SCENARIO G — nothing known is not the same as fine.
|
||||
func TestAppDumpVerdict_NoResultForThisApp_IsNoVerdict(t *testing.T) {
|
||||
dump := &backup.DBDumpStatus{Success: true, Results: []appbackup.DumpResult{res("other", nil)}}
|
||||
if got := appDumpVerdict(dump, "appWithNoDatabase"); got != "" {
|
||||
t.Errorf("an app with no dump result of its own got the verdict %q; want \"\" (no icon).\n"+
|
||||
"A green tick standing for \"a restore point file exists\" is the presence-is-not-success "+
|
||||
"rule as a UI badge.", got)
|
||||
}
|
||||
// and with no dump run recorded at all
|
||||
if got := appDumpVerdict(nil, "anything"); got != "" {
|
||||
t.Errorf("no dump status at all gave the verdict %q; want \"\"", got)
|
||||
}
|
||||
}
|
||||
|
||||
// An app with SEVERAL databases: any failure among them makes the app's backup a failure. A partial
|
||||
// dump is not a success, and reporting the last-listed result would make the verdict order-dependent.
|
||||
func TestAppDumpVerdict_AnyFailingDatabaseFailsTheApp(t *testing.T) {
|
||||
for _, tc := range []struct {
|
||||
name string
|
||||
results []appbackup.DumpResult
|
||||
}{
|
||||
{"failure first", []appbackup.DumpResult{res("app", errors.New("boom")), res("app", nil)}},
|
||||
{"failure last", []appbackup.DumpResult{res("app", nil), res("app", errors.New("boom"))}},
|
||||
} {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
if got := appDumpVerdict(&backup.DBDumpStatus{Results: tc.results}, "app"); got != "error" {
|
||||
t.Errorf("one of the app's databases failed to dump, verdict = %q, want \"error\"", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// All of this app's databases dumped cleanly → ok.
|
||||
func TestAppDumpVerdict_AllCleanIsOK(t *testing.T) {
|
||||
dump := &backup.DBDumpStatus{Results: []appbackup.DumpResult{res("app", nil), res("app", nil)}}
|
||||
if got := appDumpVerdict(dump, "app"); got != "ok" {
|
||||
t.Errorf("verdict = %q, want \"ok\"", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user