v0.5.0-rc1: slice 5 Phase A — storage observe/report + watchdog (read-only, live)

Fill the slice-3 storage_targets stub and add the fast-poll storage watchdog.
Read-only this phase; the host-root surface (mounts/SMART/grow/destructive gate)
is Phase B. Hub-owned desired manifest is slice 10, so reconcile against it is
built-but-unfed.

- internal/storage: StorageTarget wire contract, durable_id derivation per type,
  HostReader seam (procfs/sysfs, root-free), Observer (storage_targets from
  ListStorage/NodeStorage + host reads, lvmthin thin-pool fill), and the watchdog
  (third daemon goroutine; debounced out-of-band report on a known target's
  attach/disconnect transition).
- proxmox.Storage: additive parse-only config fields (durable_id sources).
- collector StorageObserver seam; Loop.SetTrigger out-of-band report; daemon runs
  the watchdog as a third goroutine; StorageConfig knobs.
- cross-repo golden kept byte-identical with felhom.eu/hub; bidirectional key-set test.

Co-Authored-By: Claude Opus 4.8 (1M context) <noreply@anthropic.com>
This commit is contained in:
2026-06-09 09:59:05 +02:00
parent 1af21a6cac
commit 27b68f043b
22 changed files with 2129 additions and 103 deletions
+27
View File
@@ -122,6 +122,33 @@ func TestLoop_RunImmediateAndResilientAfterError(t *testing.T) {
}
}
func TestLoop_OutOfBandTriggerReportsImmediately(t *testing.T) {
// The storage watchdog's trigger channel runs an extra report between ticks (the slow
// cadence is 1h here, so any report within the window comes from the trigger).
var cn, rn int32
loop := NewLoop(
&fakeCollector{report: &HostReport{}, n: &cn},
&fakeReporter{env: &ControlEnvelope{}, n: &rn},
time.Hour, quietLogger())
trigger := make(chan struct{}, 1)
loop.SetTrigger(trigger)
ctx, cancel := context.WithCancel(context.Background())
done := make(chan error, 1)
go func() { done <- loop.Run(ctx) }()
// Immediate report fires first (1 collect). Then fire the trigger → one more report.
time.Sleep(20 * time.Millisecond)
trigger <- struct{}{}
time.Sleep(20 * time.Millisecond)
cancel()
<-done
if got := atomic.LoadInt32(&cn); got < 2 {
t.Errorf("collect calls = %d, want ≥2 (immediate + out-of-band trigger)", got)
}
}
func TestLoop_RunAdoptsSlowerInterval(t *testing.T) {
var cn, rn int32
loop := NewLoop(