R-366 slice 2 (decision 168): one operator line when the box's whole-guest archives include another key's (edge-triggered)
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:
@@ -0,0 +1,73 @@
|
||||
package api
|
||||
|
||||
import (
|
||||
"net/http"
|
||||
"strings"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"gitea.dooplex.hu/admin/felhom-hub/internal/store"
|
||||
)
|
||||
|
||||
// R-366 slice 2 (`09` §3 decision 168): archives the box's restore-test skipped as written with ANOTHER key reach the
|
||||
// operator as ONE line per change of the set — not per archive, not per report; never mailed (info), never the
|
||||
// household's.
|
||||
|
||||
func reportWithForeign(foreign string) string {
|
||||
body := validReportBody("h1")
|
||||
return strings.TrimSuffix(body, "}") + `,"foreign_key_archives":{"tiers":` + foreign + `}}`
|
||||
}
|
||||
|
||||
func foreignEvents(t *testing.T, st *store.Store) []store.Event {
|
||||
t.Helper()
|
||||
ev, err := st.GetEventsByType("c1", eventForeignKeyArchives, time.Now().Add(-time.Hour))
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return ev
|
||||
}
|
||||
|
||||
// COMPANION RED-PROOF (observed): remove the noteForeignKeyArchives call from handleHostReport → this fails with
|
||||
// "a new set of other-key archives must record exactly one operator event; got 0". Restored.
|
||||
func TestR366_ForeignKeyArchivesOneEventPerChange(t *testing.T) {
|
||||
h, st, _ := newTestHandler(t)
|
||||
st.SaveCustomerConfig(&store.CustomerConfig{CustomerID: "c1", APIKey: "ckey", RetrievalPassword: "p"})
|
||||
st.UpsertHost(&store.Host{HostID: "h1", CustomerID: "c1", APIKey: "HKEY"})
|
||||
set := `[{"target":"felhom-pbs","count":2,"oldest":"2026-08-18T03:58:43Z","newest":"2026-08-19T03:58:43Z"}]`
|
||||
|
||||
for i := 0; i < 3; i++ { // the same set on three reports → one event
|
||||
if rr := do(h, http.MethodPost, "/host-report", "HKEY", reportWithForeign(set)); rr.Code != http.StatusOK {
|
||||
t.Fatalf("report %d: %d %s", i, rr.Code, rr.Body.String())
|
||||
}
|
||||
}
|
||||
ev := foreignEvents(t, st)
|
||||
if len(ev) != 1 {
|
||||
t.Fatalf("a new set of other-key archives must record exactly one operator event; got %d", len(ev))
|
||||
}
|
||||
if ev[0].Severity != "info" || !strings.Contains(ev[0].Message, "2 whole-guest backup archive(s)") ||
|
||||
!strings.Contains(ev[0].Message, "felhom-pbs (2026-08-18 … 2026-08-19)") {
|
||||
t.Fatalf("the line must name the count, the tier and the date range; got %s %q", ev[0].Severity, ev[0].Message)
|
||||
}
|
||||
|
||||
// An older agent or a fresh restart (the stanza absent) keeps the state: no new event.
|
||||
do(h, http.MethodPost, "/host-report", "HKEY", validReportBody("h1"))
|
||||
if n := len(foreignEvents(t, st)); n != 1 {
|
||||
t.Fatalf("an absent stanza must keep the state; got %d events", n)
|
||||
}
|
||||
|
||||
// The set changes (a third archive) → one more line.
|
||||
do(h, http.MethodPost, "/host-report", "HKEY", reportWithForeign(`[{"target":"felhom-pbs","count":3,"oldest":"2026-08-18T03:58:43Z","newest":"2026-08-25T03:58:43Z"}]`))
|
||||
if n := len(foreignEvents(t, st)); n != 2 {
|
||||
t.Fatalf("a changed set must record one more line; got %d events", n)
|
||||
}
|
||||
|
||||
// Evaluated, none left → the state clears silently; the same set coming back later is news again.
|
||||
do(h, http.MethodPost, "/host-report", "HKEY", reportWithForeign(`[]`))
|
||||
if n := len(foreignEvents(t, st)); n != 2 {
|
||||
t.Fatalf("an empty list clears without an event; got %d", n)
|
||||
}
|
||||
do(h, http.MethodPost, "/host-report", "HKEY", reportWithForeign(set))
|
||||
if n := len(foreignEvents(t, st)); n != 3 {
|
||||
t.Fatalf("after a clear, a set is news again; got %d", n)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user