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.
131 lines
5.5 KiB
Go
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)
|
|
}
|
|
}
|