Files
mesh-controller/internal/inventory/data_test.go
T
jochen abf9125689 Measure each item its own way; never compare a partial size (hq ADR 0233)
A walk over a large library every hour loads the array that protects it. An item now says how it
is measured — a bounded daily walk, a dataset's counters, or its top level only — and a size that
is a lower bound is kept as such and never read as a shrink.
2026-10-06 17:00:22 +02:00

131 lines
5.5 KiB
Go

package inventory
import (
"testing"
"time"
)
func size(n int64) *int64 { return &n }
func at(t time.Time) *time.Time { return &t }
// What a machine declares is kept with what its holder measured; an irreplaceable item it no longer
// declares — its module unassigned — is RETIRED and kept, never forgotten, and comes back as it was when
// declared again; anything else no longer declared is forgotten (novox/hq ADR 0233).
func TestAnUndeclaredIrreplaceableItemIsRetiredNotForgotten(t *testing.T) {
inv := fresh(t)
ctx := t.Context()
now := time.Date(2026, 10, 6, 12, 0, 0, 0, time.UTC)
declared := []DeclaredData{
{Module: "home-assistant", Item: "config", Class: "irreplaceable", Owned: true},
{Module: "searxng", Item: "valkey", Class: "cache"},
{Module: "ollama", Item: "models", Class: "rebuildable"},
}
measured := map[string]map[string]Measurement{"home-assistant": {"config": {Path: "/var/lib/home-assistant/config",
Size: size(900 << 20), LastWrite: at(now.Add(-time.Minute)), MeasuredAt: at(now), LastBackup: at(now.Add(-6 * time.Hour))}}}
if _, err := inv.RecordData(ctx, "home", declared, measured, "", now); err != nil {
t.Fatal(err)
}
all, err := inv.Data(ctx)
if err != nil || len(all) != 3 {
t.Fatalf("%+v, %v", all, err)
}
// Unassigned: nothing of it is declared any more.
later := now.Add(time.Hour)
change, err := inv.RecordData(ctx, "home", nil, nil, "home-assistant unassigned", later)
if err != nil {
t.Fatal(err)
}
if len(change.Retired) != 1 || change.Retired[0].Item != "config" {
t.Fatalf("retired %+v", change.Retired)
}
all, err = inv.Data(ctx)
if err != nil {
t.Fatal(err)
}
if len(all) != 1 || !all[0].Retired() || all[0].Path != "/var/lib/home-assistant/config" ||
*all[0].Size != 900<<20 || all[0].RetiredWhy != "home-assistant unassigned" {
t.Fatalf("the irreplaceable item is not kept retired with where it is: %+v", all)
}
// A second run that still does not declare it changes nothing: retired once, not again.
change, err = inv.RecordData(ctx, "home", nil, nil, "again", later.Add(time.Hour))
if err != nil || len(change.Retired) != 0 {
t.Fatalf("retired twice: %+v, %v", change, err)
}
// Deleting needs it retired; assigned again it is not retired any more, and its history stays.
if err := inv.MarkDataDeleted(ctx, "home", "searxng", "valkey", "p", "w", later); err == nil {
t.Fatal("deleting something the mesh does not hold retired was recorded")
}
change, err = inv.RecordData(ctx, "home", declared[:1], nil, "", later.Add(2*time.Hour))
if err != nil || len(change.Reenabled) != 1 {
t.Fatalf("declared again: %+v, %v", change, err)
}
r, err := inv.DataOf(ctx, "home", "home-assistant", "config")
if err != nil || r.Retired() || !r.FirstSeen.Equal(now) || r.Size == nil {
t.Fatalf("declared again lost what was known: %+v, %v", r, err)
}
// Retired and then deleted by a person: recorded, never by dropping the row.
if _, err := inv.RecordData(ctx, "home", nil, nil, "unassigned again", later.Add(3*time.Hour)); err != nil {
t.Fatal(err)
}
if err := inv.MarkDataDeleted(ctx, "home", "home-assistant", "config", "jochen", "moved to the anchor", later.Add(4*time.Hour)); err != nil {
t.Fatal(err)
}
r, err = inv.DataOf(ctx, "home", "home-assistant", "config")
if err != nil || r.DeletedAt == nil || r.DeletedBy != "jochen" || r.Retired() {
t.Fatalf("a deletion is not on record: %+v, %v", r, err)
}
}
// A reading is kept at most once an hour, and the peak is read over the window asked.
func TestReadingsAreHourlyAndThePeakIsTheLargest(t *testing.T) {
inv := fresh(t)
ctx := t.Context()
now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
declared := []DeclaredData{{Module: "grafana", Item: "data", Class: "valuable", Owned: true}}
for i, s := range []int64{100, 120, 999, 130, 40} {
when := now.Add(time.Duration(i) * 20 * time.Minute)
if _, err := inv.RecordData(ctx, "ace", declared, map[string]map[string]Measurement{"grafana": {"data": {
Path: "/var/lib/grafana/data", Size: size(s), MeasuredAt: at(when)}}}, "", when); err != nil {
t.Fatal(err)
}
}
var n int
if err := inv.store.Pool().QueryRow(ctx, `select count(*) from data_reading`).Scan(&n); err != nil || n != 2 {
t.Fatalf("%d readings kept for five measurements over 80 minutes, want 2 (one an hour): %v", n, err)
}
peaks, err := inv.DataPeaks(ctx, now.Add(-time.Hour))
if err != nil || peaks["ace/grafana/data"] != 130 {
t.Fatalf("peak %v, %v", peaks, err)
}
r, _ := inv.DataOf(ctx, "ace", "grafana", "data")
if r.Size == nil || *r.Size != 40 {
t.Fatalf("the newest measurement is not the one on record: %+v", r)
}
}
// A partial walk's lower bound is kept as the newest measurement, said as partial, and never kept as a
// reading a shrink would be read against.
func TestAPartialMeasurementIsNeverAReading(t *testing.T) {
inv := fresh(t)
ctx := t.Context()
now := time.Date(2026, 10, 6, 0, 0, 0, 0, time.UTC)
declared := []DeclaredData{{Module: "big", Item: "data", Class: "valuable", Owned: true}}
if _, err := inv.RecordData(ctx, "home", declared, map[string]map[string]Measurement{"big": {"data": {
Path: "/srv/big", Size: size(5), MeasuredAt: at(now), Precision: "partial: stopped"}}}, "", now); err != nil {
t.Fatal(err)
}
var n int
if err := inv.store.Pool().QueryRow(ctx, `select count(*) from data_reading`).Scan(&n); err != nil || n != 0 {
t.Fatalf("%d readings from a partial measurement: %v", n, err)
}
r, err := inv.DataOf(ctx, "home", "big", "data")
if err != nil || r.Precision != "partial: stopped" || Comparable(r.Precision) {
t.Fatalf("%+v, %v", r, err)
}
}