Files
felhom-controller/controller/internal/web/backup_target_offer_test.go
T
admin 3f7cf2a965 v0.185.0 — E-2 Parts 3+4: the offer, and the honest degraded state
The half that makes the rest work: a degraded backup target recorded only in
config is the silent-degradation pattern this arc has spent a week removing.

Part 3 -- POST /api/backup-target/assign moves the target via the agent's
POST /backup/target. It is the ONLY writer of the role: registration does not set
it, the drive-gate does not, no scheduler does. Declining is not calling it. The
agent returns restart_required rather than restarting itself, because restarting
with a backup in flight records a spurious tier failure for a backup that
actually succeeded (E-1 did exactly that).

Part 4 -- GET /api/backup-target returns the state and, when degraded, Hungarian
copy in FACT -> CONSEQUENCE -> REMEDY order, pinned by a test: a customer told
only the fact cannot act on it.

Healthy renders NOTHING -- no badge, no reassurance, no tonal change.
degradedMessageFor is the single decision point, so exactly one place could start
decorating a working box. Red-proofed: reassuring on the healthy branch fails
Scenario E.

UNKNOWN is not degraded: an unreachable or pre-R-82 agent means we could not ask,
which is not evidence of degradation (R-88 Part 2's class).

A HOLLOW TEST caught by its own red-proof: TestUnknownStateRendersNothing used
{Known:false} with Degraded left false, so it passed even with the !Known guard
deleted -- the second condition covered for it. Now {Known:false, Degraded:true},
which fails properly. Without the red-proof the test would have been decoration.

State is derived from the AGENT, never from our intent flag: on the two boxes
migrated by hand in E-1 the intent was never recorded while the drive really is
the target.

MinAgent: 0.113.0
Green gate: build + vet + test rc=0 (27 packages), run separately from this commit.
2026-07-29 09:10:20 +02:00

62 lines
3.0 KiB
Go

package web
import (
"strings"
"testing"
)
// SCENARIO E — a healthy configuration must look NORMAL.
//
// The single most likely over-correction in this whole arc: decorating every dashboard with a
// reassurance banner. Eight consecutive fixes here have produced one, and a permanent notice on a
// working box is how a real warning stops being read. When the target is a real drive, the state
// carries no message at all — there is nothing for the UI to render.
func TestHealthyBackupTargetRendersNothing(t *testing.T) {
st := BackupTargetState{Known: true, Degraded: false, TargetID: "felhom-backup", Label: "Külső HDD"}
if msg := degradedMessageFor(st); msg != "" {
t.Fatalf("a healthy target produced customer copy %q — a working configuration must look "+
"normal, with no caution and no decoration (Scenario E)", msg)
}
}
// SCENARIO D — declining is valid; forgetting is not. A degraded box keeps saying so, every time it
// is asked. There is no "dismissed" flag by design: the customer declines by not accepting, and the
// state is recomputed from the agent on the next visit rather than remembered as handled.
func TestDegradedStateKeepsReportingOnEveryVisit(t *testing.T) {
st := BackupTargetState{Known: true, Degraded: true, TargetID: "local"}
for visit := 1; visit <= 3; visit++ {
if msg := degradedMessageFor(st); msg == "" {
t.Fatalf("visit %d produced no message — a declined offer must stay VISIBLE, not go quiet", visit)
}
}
}
// The copy must carry FACT → CONSEQUENCE → REMEDY. A customer told only "your backup is on the same
// disk" cannot act; the sentence has to say what that costs them and what fixes it.
func TestDegradedCopyNamesConsequenceAndRemedy(t *testing.T) {
msg := degradedMessageFor(BackupTargetState{Known: true, Degraded: true})
for _, want := range []struct{ frag, why string }{
{"ugyanazon a lemezen", "the FACT — it shares the disk with the system"},
{"lemezhiba ellen nem", "the CONSEQUENCE — it does not survive a disk failure"},
{"második meghajtót", "the REMEDY — attach a second drive"},
} {
if !strings.Contains(msg, want.frag) {
t.Errorf("degraded copy is missing %s (%q); got: %s", want.why, want.frag, msg)
}
}
}
// UNKNOWN IS NOT DEGRADED. An unreachable or pre-R-82 agent means we could not ask — rendering a
// warning there would put a permanent scare on a box that may be perfectly healthy, and it is the
// absence-read-as-a-value mistake this project keeps closing (R-88 Part 2).
func TestUnknownStateRendersNothing(t *testing.T) {
// Degraded:true is deliberate. With Degraded:false the fixture passes even if the !Known guard is
// deleted, because the second condition catches it — the test would be HOLLOW and a red-proof
// proved exactly that. The meaningful case is "we could not ask, and the other field says
// degraded": the guard must win.
if msg := degradedMessageFor(BackupTargetState{Known: false, Degraded: true}); msg != "" {
t.Fatalf("an UNKNOWN state produced customer copy %q — not being able to ask is not evidence "+
"of degradation", msg)
}
}