Pulse/internal/truenas/provider_test.go
2026-06-29 22:54:19 +01:00

1288 lines
43 KiB
Go

package truenas
import (
"context"
"errors"
"net/http"
"testing"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/storagehealth"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
type stubFetcher struct {
snapshot *FixtureSnapshot
err error
calls int
}
func (s *stubFetcher) Fetch(context.Context) (*FixtureSnapshot, error) {
s.calls++
if s.err != nil {
return nil, s.err
}
return copyFixtureSnapshot(s.snapshot), nil
}
type closableStubFetcher struct {
closeCalls int
}
func (s *closableStubFetcher) Fetch(context.Context) (*FixtureSnapshot, error) {
return nil, nil
}
func (s *closableStubFetcher) Close() {
s.closeCalls++
}
type controllableStubFetcher struct {
snapshot *FixtureSnapshot
startCalls []string
stopCalls []string
logReads []appLogReadCall
diskHistory map[string][]TimeSeriesPoint
systemHistory *SystemMetricHistory
}
type appLogReadCall struct {
appName string
containerID string
tailLines int
}
func (s *controllableStubFetcher) Fetch(context.Context) (*FixtureSnapshot, error) {
return copyFixtureSnapshot(s.snapshot), nil
}
func (s *controllableStubFetcher) StartApp(_ context.Context, appID string) error {
s.startCalls = append(s.startCalls, appID)
for i := range s.snapshot.Apps {
if s.snapshot.Apps[i].ID == appID {
s.snapshot.Apps[i].State = "RUNNING"
if len(s.snapshot.Apps[i].Containers) > 0 {
s.snapshot.Apps[i].Containers[0].State = "running"
}
}
}
return nil
}
func (s *controllableStubFetcher) StopApp(_ context.Context, appID string) error {
s.stopCalls = append(s.stopCalls, appID)
for i := range s.snapshot.Apps {
if s.snapshot.Apps[i].ID == appID {
s.snapshot.Apps[i].State = "STOPPED"
if len(s.snapshot.Apps[i].Containers) > 0 {
s.snapshot.Apps[i].Containers[0].State = "stopped"
}
}
}
return nil
}
func (s *controllableStubFetcher) ReadAppLogs(_ context.Context, appName, containerID string, tailLines int) ([]AppLogLine, error) {
s.logReads = append(s.logReads, appLogReadCall{
appName: appName,
containerID: containerID,
tailLines: tailLines,
})
return []AppLogLine{
{Timestamp: "2026-03-29T18:00:00Z", Data: "ready"},
{Timestamp: "2026-03-29T18:01:00Z", Data: "serving"},
}, nil
}
func (s *controllableStubFetcher) DiskTemperatureHistory(_ context.Context, identifiers []string, _ time.Duration) (map[string][]TimeSeriesPoint, error) {
if len(s.diskHistory) == 0 {
return nil, nil
}
result := make(map[string][]TimeSeriesPoint)
for _, identifier := range identifiers {
points, ok := s.diskHistory[identifier]
if !ok || len(points) == 0 {
continue
}
copied := make([]TimeSeriesPoint, len(points))
copy(copied, points)
result[identifier] = copied
}
if len(result) == 0 {
return nil, nil
}
return result, nil
}
func (s *controllableStubFetcher) SystemMetricHistory(context.Context, time.Duration) (*SystemMetricHistory, error) {
if s == nil || s.systemHistory == nil {
return nil, nil
}
copied := *s.systemHistory
copied.CPUPercent = cloneTimeSeriesPoints(s.systemHistory.CPUPercent)
copied.MemoryPercent = cloneTimeSeriesPoints(s.systemHistory.MemoryPercent)
copied.MemoryUsedBytes = cloneTimeSeriesPoints(s.systemHistory.MemoryUsedBytes)
copied.MemoryAvailableBytes = cloneTimeSeriesPoints(s.systemHistory.MemoryAvailableBytes)
copied.MemoryTotalBytes = cloneTimeSeriesPoints(s.systemHistory.MemoryTotalBytes)
copied.NetInRate = cloneTimeSeriesPoints(s.systemHistory.NetInRate)
copied.NetOutRate = cloneTimeSeriesPoints(s.systemHistory.NetOutRate)
copied.DiskReadRate = cloneTimeSeriesPoints(s.systemHistory.DiskReadRate)
copied.DiskWriteRate = cloneTimeSeriesPoints(s.systemHistory.DiskWriteRate)
return &copied, nil
}
func TestFixtureFetcherReturnsSnapshotCopy(t *testing.T) {
fixtures := DefaultFixtures()
fetcher := &FixtureFetcher{Snapshot: fixtures}
first, err := fetcher.Fetch(context.Background())
if err != nil {
t.Fatalf("Fetch() error = %v", err)
}
if first == nil {
t.Fatal("expected snapshot")
}
first.Pools[0].Name = "mutated"
first.Datasets = append(first.Datasets, Dataset{Name: "extra/dataset"})
first.Services[0].PIDs[0] = 9999
first.System.TemperatureCelsius["cpu_package"] = 99.9
second, err := fetcher.Fetch(context.Background())
if err != nil {
t.Fatalf("Fetch() second error = %v", err)
}
if second == nil {
t.Fatal("expected second snapshot")
}
if second.Pools[0].Name != fixtures.Pools[0].Name {
t.Fatalf("expected fixture pool name %q, got %q", fixtures.Pools[0].Name, second.Pools[0].Name)
}
if len(second.Datasets) != len(fixtures.Datasets) {
t.Fatalf("expected dataset count %d, got %d", len(fixtures.Datasets), len(second.Datasets))
}
if second.Services[0].PIDs[0] != fixtures.Services[0].PIDs[0] {
t.Fatalf("expected fixture service PID %d, got %d", fixtures.Services[0].PIDs[0], second.Services[0].PIDs[0])
}
if second.System.TemperatureCelsius["cpu_package"] != fixtures.System.TemperatureCelsius["cpu_package"] {
t.Fatalf("expected fixture cpu_package temperature %v, got %v", fixtures.System.TemperatureCelsius["cpu_package"], second.System.TemperatureCelsius["cpu_package"])
}
}
func TestAPIFetcherDelegatesToClientFetchSnapshot(t *testing.T) {
server := newMockServer(t, defaultAPIResponses(), nil)
t.Cleanup(server.Close)
client := mustClientForServer(t, server.URL, ClientConfig{APIKey: "api-key"})
fetcher := &APIFetcher{Client: client}
snapshot, err := fetcher.Fetch(context.Background())
if err != nil {
t.Fatalf("Fetch() error = %v", err)
}
if snapshot == nil {
t.Fatal("expected snapshot")
}
if snapshot.System.Hostname != "truenas-main" {
t.Fatalf("unexpected hostname: %q", snapshot.System.Hostname)
}
}
func TestProviderRefreshUpdatesLastSnapshot(t *testing.T) {
server := newMockServer(t, defaultAPIResponses(), nil)
t.Cleanup(server.Close)
client := mustClientForServer(t, server.URL, ClientConfig{APIKey: "api-key"})
provider := NewLiveProvider(&APIFetcher{Client: client})
if err := provider.Refresh(context.Background()); err != nil {
t.Fatalf("Refresh() error = %v", err)
}
provider.mu.Lock()
snapshot := copyFixtureSnapshot(provider.lastSnapshot)
provider.mu.Unlock()
if snapshot == nil {
t.Fatal("expected cached snapshot")
}
if snapshot.System.Hostname != "truenas-main" {
t.Fatalf("unexpected cached hostname: %q", snapshot.System.Hostname)
}
if len(snapshot.Pools) != 1 || len(snapshot.Datasets) != 1 {
t.Fatalf("unexpected cached counts: pools=%d datasets=%d", len(snapshot.Pools), len(snapshot.Datasets))
}
}
func TestProviderRecordsSynthesizesIncidentFromUnhealthyPoolStatus(t *testing.T) {
collectedAt := time.Date(2026, 6, 29, 12, 0, 0, 0, time.UTC)
records := FixtureRecords(FixtureSnapshot{
CollectedAt: collectedAt,
System: SystemInfo{
Hostname: "truenas-main",
Healthy: true,
},
Pools: []Pool{{
ID: "pool-tank",
Name: "tank",
Status: "DEGRADED",
TotalBytes: 1000,
UsedBytes: 400,
FreeBytes: 600,
}},
})
record := requirePoolRecord(t, records, "tank")
if record.Resource.Status != unifiedresources.StatusWarning {
t.Fatalf("pool status = %q, want warning", record.Resource.Status)
}
if len(record.Resource.Incidents) != 1 {
t.Fatalf("expected one synthesized pool incident, got %+v", record.Resource.Incidents)
}
incident := record.Resource.Incidents[0]
if incident.Code != "zfs_pool_state" {
t.Fatalf("incident code = %q, want zfs_pool_state", incident.Code)
}
if incident.NativeID != "pool:tank:state" {
t.Fatalf("incident native id = %q, want pool:tank:state", incident.NativeID)
}
if incident.Source != "pool.query" {
t.Fatalf("incident source = %q, want pool.query", incident.Source)
}
if incident.Severity != storagehealth.RiskWarning {
t.Fatalf("incident severity = %q, want warning", incident.Severity)
}
if incident.Summary != "ZFS pool tank is DEGRADED" {
t.Fatalf("incident summary = %q", incident.Summary)
}
if !incident.StartedAt.Equal(collectedAt) {
t.Fatalf("incident started at = %s, want %s", incident.StartedAt, collectedAt)
}
}
func TestProviderRecordsDoesNotDuplicateNativePoolAlert(t *testing.T) {
alertTime := time.Date(2026, 6, 29, 11, 30, 0, 0, time.UTC)
records := FixtureRecords(FixtureSnapshot{
CollectedAt: time.Date(2026, 6, 29, 12, 0, 0, 0, time.UTC),
System: SystemInfo{
Hostname: "truenas-main",
Healthy: true,
},
Pools: []Pool{{
ID: "pool-tank",
Name: "tank",
Status: "DEGRADED",
TotalBytes: 1000,
UsedBytes: 400,
FreeBytes: 600,
}},
Alerts: []Alert{{
ID: "native-pool-alert",
Level: "WARNING",
Source: "VolumeStatus",
Message: "Pool tank is DEGRADED: one member is faulted.",
Datetime: alertTime,
}},
})
record := requirePoolRecord(t, records, "tank")
if len(record.Resource.Incidents) != 1 {
t.Fatalf("expected one native pool incident, got %+v", record.Resource.Incidents)
}
incident := record.Resource.Incidents[0]
if incident.Code != "truenas_volume_status" {
t.Fatalf("incident code = %q, want truenas_volume_status", incident.Code)
}
if incident.NativeID != "native-pool-alert" {
t.Fatalf("incident native id = %q, want native-pool-alert", incident.NativeID)
}
if incident.Source != "VolumeStatus" {
t.Fatalf("incident source = %q, want VolumeStatus", incident.Source)
}
if !incident.StartedAt.Equal(alertTime) {
t.Fatalf("incident started at = %s, want %s", incident.StartedAt, alertTime)
}
}
func TestProviderControlAppUsesFetcherAndRefreshesSnapshot(t *testing.T) {
fixtures := DefaultFixtures()
for i := range fixtures.Apps {
if fixtures.Apps[i].ID == "nextcloud" {
fixtures.Apps[i].State = "STOPPED"
if len(fixtures.Apps[i].Containers) > 0 {
fixtures.Apps[i].Containers[0].State = "stopped"
}
}
}
fetcher := &controllableStubFetcher{snapshot: &fixtures}
provider := NewLiveProvider(fetcher)
snapshot, err := provider.ControlApp(context.Background(), "nextcloud", "start")
if err != nil {
t.Fatalf("ControlApp(start) error = %v", err)
}
if len(fetcher.startCalls) != 1 || fetcher.startCalls[0] != "nextcloud" {
t.Fatalf("expected start call for nextcloud, got %+v", fetcher.startCalls)
}
if snapshot == nil {
t.Fatal("expected refreshed snapshot after control")
}
foundRunning := false
for _, app := range snapshot.Apps {
if app.ID == "nextcloud" && app.State == "RUNNING" {
foundRunning = true
}
}
if !foundRunning {
t.Fatalf("expected nextcloud to be RUNNING after control, got %+v", snapshot.Apps)
}
}
func TestProviderReadAppLogsUsesCanonicalPrimaryContainer(t *testing.T) {
fixtures := DefaultFixtures()
fetcher := &controllableStubFetcher{snapshot: &fixtures}
provider := NewLiveProvider(fetcher)
if err := provider.Refresh(context.Background()); err != nil {
t.Fatalf("Refresh() error = %v", err)
}
result, err := provider.ReadAppLogs(context.Background(), "nextcloud", "", 25)
if err != nil {
t.Fatalf("ReadAppLogs() error = %v", err)
}
if result == nil {
t.Fatal("expected app log result")
}
if result.Host != "truenas-main" || result.App.Name != "Nextcloud" {
t.Fatalf("unexpected app log result identity: %+v", result)
}
if result.Container.ServiceName != "nextcloud" || result.Container.ID != "nextcloud-web-1" {
t.Fatalf("expected canonical primary container, got %+v", result.Container)
}
if len(fetcher.logReads) != 1 {
t.Fatalf("expected one log read, got %+v", fetcher.logReads)
}
if call := fetcher.logReads[0]; call.appName != "nextcloud" || call.containerID != "nextcloud-web-1" || call.tailLines != 25 {
t.Fatalf("unexpected log read call: %+v", call)
}
}
func TestProviderReadAppLogsSupportsExplicitServiceSelection(t *testing.T) {
fixtures := DefaultFixtures()
fetcher := &controllableStubFetcher{snapshot: &fixtures}
provider := NewLiveProvider(fetcher)
if err := provider.Refresh(context.Background()); err != nil {
t.Fatalf("Refresh() error = %v", err)
}
result, err := provider.ReadAppLogs(context.Background(), "nextcloud", "redis", 10)
if err != nil {
t.Fatalf("ReadAppLogs() error = %v", err)
}
if result == nil {
t.Fatal("expected app log result")
}
if result.Container.ServiceName != "redis" || result.Container.ID != "nextcloud-redis-1" {
t.Fatalf("expected explicit redis container selection, got %+v", result.Container)
}
if len(fetcher.logReads) != 1 || fetcher.logReads[0].containerID != "nextcloud-redis-1" {
t.Fatalf("unexpected log read calls: %+v", fetcher.logReads)
}
}
func TestProviderGetAppConfigReturnsCanonicalApp(t *testing.T) {
fixtures := DefaultFixtures()
fetcher := &controllableStubFetcher{snapshot: &fixtures}
provider := NewLiveProvider(fetcher)
if err := provider.Refresh(context.Background()); err != nil {
t.Fatalf("Refresh() error = %v", err)
}
result, err := provider.GetAppConfig(context.Background(), "nextcloud")
if err != nil {
t.Fatalf("GetAppConfig() error = %v", err)
}
if result == nil {
t.Fatal("expected app config result")
}
if result.Host != "truenas-main" {
t.Fatalf("expected host truenas-main, got %+v", result)
}
if result.App.ID != "nextcloud" || result.App.Name != "Nextcloud" {
t.Fatalf("unexpected app config payload: %+v", result.App)
}
if len(result.App.Containers) != 2 {
t.Fatalf("expected full app container runtime shape, got %+v", result.App.Containers)
}
}
func TestProviderRefreshPreservesLastSnapshotOnError(t *testing.T) {
initial := DefaultFixtures()
provider := NewProvider(initial)
expectedErr := errors.New("fetch failed")
provider.fetcher = &stubFetcher{err: expectedErr}
err := provider.Refresh(context.Background())
if err == nil {
t.Fatal("expected Refresh() error")
}
if !errors.Is(err, expectedErr) {
t.Fatalf("expected error %v, got %v", expectedErr, err)
}
provider.mu.Lock()
snapshot := copyFixtureSnapshot(provider.lastSnapshot)
provider.mu.Unlock()
if snapshot == nil {
t.Fatal("expected cached snapshot")
}
if snapshot.System.Hostname != initial.System.Hostname {
t.Fatalf("expected hostname %q, got %q", initial.System.Hostname, snapshot.System.Hostname)
}
if len(snapshot.Pools) != len(initial.Pools) {
t.Fatalf("expected pool count %d, got %d", len(initial.Pools), len(snapshot.Pools))
}
}
func TestProviderRefreshPreservesLastSnapshotOnNilSnapshot(t *testing.T) {
initial := DefaultFixtures()
provider := NewProvider(initial)
provider.fetcher = &stubFetcher{snapshot: nil}
err := provider.Refresh(context.Background())
if !errors.Is(err, errNilSnapshot) {
t.Fatalf("expected errNilSnapshot, got %v", err)
}
provider.mu.Lock()
snapshot := copyFixtureSnapshot(provider.lastSnapshot)
provider.mu.Unlock()
if snapshot == nil {
t.Fatal("expected cached snapshot")
}
if snapshot.System.Hostname != initial.System.Hostname {
t.Fatalf("expected hostname %q, got %q", initial.System.Hostname, snapshot.System.Hostname)
}
if len(snapshot.Pools) != len(initial.Pools) {
t.Fatalf("expected pool count %d, got %d", len(initial.Pools), len(snapshot.Pools))
}
}
func TestRecordsDoesNotCallRefreshWhenSnapshotMissing(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
stub := &stubFetcher{}
provider := NewLiveProvider(stub)
records := provider.Records()
if records != nil {
t.Fatalf("expected nil records when no snapshot is cached, got %d records", len(records))
}
if stub.calls != 0 {
t.Fatalf("expected Records() to avoid fetch calls, got %d", stub.calls)
}
}
func TestSystemRecordPopulatesTrueNASMetadata(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
provider := NewProvider(DefaultFixtures())
records := provider.Records()
if len(records) == 0 {
t.Fatal("expected fixture records from provider")
}
system := records[0]
if system.Resource.Type != unifiedresources.ResourceTypeAgent {
t.Fatalf("expected first record type host, got %s", system.Resource.Type)
}
if system.Resource.TrueNAS == nil {
t.Fatal("expected TrueNAS metadata on system record")
}
if system.Resource.TrueNAS.UptimeSeconds != int64(42*24*60*60) {
t.Fatalf("expected uptime %d, got %d", int64(42*24*60*60), system.Resource.TrueNAS.UptimeSeconds)
}
if system.Resource.TrueNAS.Version != "TrueNAS-SCALE-24.10.2" {
t.Fatalf("expected version %q, got %q", "TrueNAS-SCALE-24.10.2", system.Resource.TrueNAS.Version)
}
if system.Resource.TrueNAS.Hostname != "truenas-main" {
t.Fatalf("expected hostname %q, got %q", "truenas-main", system.Resource.TrueNAS.Hostname)
}
if system.Resource.Agent == nil {
t.Fatal("expected canonical agent payload on TrueNAS system record")
}
if system.Resource.Agent.Platform != "truenas" || system.Resource.Agent.OSName != "TrueNAS" {
t.Fatalf("expected canonical TrueNAS agent metadata, got %+v", system.Resource.Agent)
}
if system.Resource.Agent.CPUCount != 16 || system.Resource.Agent.UptimeSeconds != int64(42*24*60*60) {
t.Fatalf("expected canonical host telemetry metadata, got %+v", system.Resource.Agent)
}
if system.Resource.Agent.Temperature == nil || *system.Resource.Agent.Temperature != 61.5 {
t.Fatalf("expected canonical host temperature 61.5, got %+v", system.Resource.Agent)
}
if system.Resource.Agent.Sensors == nil || system.Resource.Agent.Sensors.TemperatureCelsius["cpu_package"] != 61.5 {
t.Fatalf("expected canonical host sensors on TrueNAS system record, got %+v", system.Resource.Agent.Sensors)
}
if system.Resource.Metrics == nil || system.Resource.Metrics.CPU == nil || system.Resource.Metrics.Memory == nil {
t.Fatalf("expected canonical system metrics on TrueNAS host record, got %+v", system.Resource.Metrics)
}
if system.Resource.Metrics.CPU.Percent != 38 {
t.Fatalf("expected cpu percent 38, got %+v", system.Resource.Metrics.CPU)
}
if system.Resource.Metrics.NetIn == nil || system.Resource.Metrics.NetIn.Value != 48_000_000 {
t.Fatalf("expected network telemetry metrics, got %+v", system.Resource.Metrics)
}
}
func TestRecordsProjectTrueNASAppStatsIntoCanonicalMetrics(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
provider := NewProvider(DefaultFixtures())
records := provider.Records()
if len(records) == 0 {
t.Fatal("expected fixture records from provider")
}
for _, record := range records {
if record.Resource.Type != unifiedresources.ResourceTypeAppContainer || record.Resource.Name != "Nextcloud" {
continue
}
if record.Resource.Metrics == nil || record.Resource.Metrics.CPU == nil || record.Resource.Metrics.CPU.Percent != 18 {
t.Fatalf("expected canonical CPU metrics on Nextcloud, got %+v", record.Resource.Metrics)
}
if record.Resource.Docker == nil || record.Resource.Docker.NetInRate != 2_100_000 || record.Resource.Docker.DiskReadRate != 320_000 {
t.Fatalf("expected projected TrueNAS app rates, got %+v", record.Resource.Docker)
}
return
}
t.Fatal("expected Nextcloud app record")
}
func TestProviderRefreshDerivesTrueNASAppDiskRatesFromPreviousSnapshot(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
initial := DefaultFixtures()
initial.CollectedAt = time.Date(2026, 2, 8, 12, 0, 0, 0, time.UTC)
initial.Apps[0].Stats.BlockReadBytes = 100
initial.Apps[0].Stats.BlockWriteBytes = 50
initial.Apps[0].Stats.CollectedAt = initial.CollectedAt
updated := DefaultFixtures()
updated.CollectedAt = initial.CollectedAt.Add(4 * time.Second)
updated.Apps[0].Stats.BlockReadBytes = 500
updated.Apps[0].Stats.BlockWriteBytes = 250
updated.Apps[0].Stats.CollectedAt = updated.CollectedAt
provider := NewProvider(initial)
provider.fetcher = &stubFetcher{snapshot: &updated}
if err := provider.Refresh(context.Background()); err != nil {
t.Fatalf("Refresh() error = %v", err)
}
snapshot := provider.Snapshot()
if snapshot == nil || snapshot.Apps[0].Stats == nil {
t.Fatal("expected refreshed app stats")
}
if snapshot.Apps[0].Stats.DiskReadRate != 100 {
t.Fatalf("expected disk read rate 100, got %v", snapshot.Apps[0].Stats.DiskReadRate)
}
if snapshot.Apps[0].Stats.DiskWriteRate != 50 {
t.Fatalf("expected disk write rate 50, got %v", snapshot.Apps[0].Stats.DiskWriteRate)
}
}
func TestAPIFetcherCloseDelegatesToClient(t *testing.T) {
transport := &closeTrackingTransport{}
client := &Client{
httpClient: &http.Client{
Transport: transport,
},
}
fetcher := &APIFetcher{Client: client}
fetcher.Close()
if transport.closeCalls != 1 {
t.Fatalf("expected CloseIdleConnections to be called once, got %d", transport.closeCalls)
}
}
func TestProviderCloseDelegatesToFetcher(t *testing.T) {
fetcher := &closableStubFetcher{}
provider := NewLiveProvider(fetcher)
provider.Close()
if fetcher.closeCalls != 1 {
t.Fatalf("expected fetcher Close() to be called once, got %d", fetcher.closeCalls)
}
}
func TestRecordsIncludeDiskResourcesWithCorrectParentChain(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
provider := NewProvider(DefaultFixtures())
records := provider.Records()
if len(records) == 0 {
t.Fatal("expected fixture records from provider")
}
var diskRecords []unifiedresources.IngestRecord
for _, record := range records {
if record.Resource.Type == unifiedresources.ResourceTypePhysicalDisk {
diskRecords = append(diskRecords, record)
}
}
fixtures := DefaultFixtures()
if len(diskRecords) != len(fixtures.Disks) {
t.Fatalf("expected %d disk records, got %d", len(fixtures.Disks), len(diskRecords))
}
var sda, nvme0n1, sdc *unifiedresources.IngestRecord
for i := range diskRecords {
record := &diskRecords[i]
switch record.Resource.Name {
case "sda":
sda = record
case "nvme0n1":
nvme0n1 = record
case "sdc":
sdc = record
}
}
if sda == nil || nvme0n1 == nil || sdc == nil {
t.Fatalf("expected to find disk records for sda, nvme0n1, and sdc")
}
if sda.ParentSourceID != "system:truenas-main/pool:tank" {
t.Fatalf("expected sda parent system:truenas-main/pool:tank, got %q", sda.ParentSourceID)
}
if sda.Resource.PhysicalDisk == nil {
t.Fatal("expected sda PhysicalDiskMeta")
}
if sda.Resource.PhysicalDisk.Model != "Seagate Exos X18" {
t.Fatalf("expected sda model %q, got %q", "Seagate Exos X18", sda.Resource.PhysicalDisk.Model)
}
if sda.Resource.PhysicalDisk.Serial != "ZL0A1234" {
t.Fatalf("expected sda serial %q, got %q", "ZL0A1234", sda.Resource.PhysicalDisk.Serial)
}
if sda.Resource.PhysicalDisk.DiskType != "sata" {
t.Fatalf("expected sda disk type %q, got %q", "sata", sda.Resource.PhysicalDisk.DiskType)
}
if sda.Resource.PhysicalDisk.Temperature != 34 {
t.Fatalf("expected sda temperature 34, got %d", sda.Resource.PhysicalDisk.Temperature)
}
if sda.Resource.PhysicalDisk.RPM != 7200 {
t.Fatalf("expected sda rpm 7200, got %d", sda.Resource.PhysicalDisk.RPM)
}
if sda.Identity.MachineID != "ZL0A1234" {
t.Fatalf("expected sda identity machine ID %q, got %q", "ZL0A1234", sda.Identity.MachineID)
}
if len(sda.Identity.Hostnames) != 1 || sda.Identity.Hostnames[0] != "truenas-main" {
t.Fatalf("expected sda identity hostname [truenas-main], got %v", sda.Identity.Hostnames)
}
if nvme0n1.ParentSourceID != "system:truenas-main/pool:fast" {
t.Fatalf("expected nvme0n1 parent system:truenas-main/pool:fast, got %q", nvme0n1.ParentSourceID)
}
if nvme0n1.Resource.PhysicalDisk == nil {
t.Fatal("expected nvme0n1 PhysicalDiskMeta")
}
if nvme0n1.Resource.PhysicalDisk.DiskType != "nvme" {
t.Fatalf("expected nvme0n1 disk type %q, got %q", "nvme", nvme0n1.Resource.PhysicalDisk.DiskType)
}
if nvme0n1.Resource.PhysicalDisk.RPM != 0 {
t.Fatalf("expected nvme0n1 rpm 0, got %d", nvme0n1.Resource.PhysicalDisk.RPM)
}
if sdc.Resource.Status != unifiedresources.StatusWarning {
t.Fatalf("expected sdc status warning, got %s", sdc.Resource.Status)
}
if sdc.Resource.PhysicalDisk == nil {
t.Fatal("expected sdc PhysicalDiskMeta")
}
if sdc.Resource.PhysicalDisk.Health != "DEGRADED" {
t.Fatalf("expected sdc health DEGRADED, got %q", sdc.Resource.PhysicalDisk.Health)
}
if sdc.Resource.PhysicalDisk.Temperature != 63 {
t.Fatalf("expected sdc temperature 63, got %d", sdc.Resource.PhysicalDisk.Temperature)
}
if sdc.Resource.PhysicalDisk.Risk == nil {
t.Fatal("expected sdc physical-disk risk")
}
foundTemperatureReason := false
foundSmartReason := false
for _, reason := range sdc.Resource.PhysicalDisk.Risk.Reasons {
if reason.Code == "temperature_high" {
foundTemperatureReason = true
}
if reason.Code == "truenas_smart" {
foundSmartReason = true
}
}
if !foundTemperatureReason {
t.Fatalf("expected sdc physical-disk risk to include temperature_high, got %+v", sdc.Resource.PhysicalDisk.Risk.Reasons)
}
if !foundSmartReason {
t.Fatalf("expected sdc physical-disk risk to include truenas_smart, got %+v", sdc.Resource.PhysicalDisk.Risk.Reasons)
}
}
func TestRecordsIncludeTrueNASAppsAsCanonicalWorkloads(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
provider := NewProvider(DefaultFixtures())
records := provider.Records()
if len(records) == 0 {
t.Fatal("expected fixture records from provider")
}
var nextcloud *unifiedresources.IngestRecord
var adguard *unifiedresources.IngestRecord
for i := range records {
record := &records[i]
if record.Resource.Type != unifiedresources.ResourceTypeAppContainer {
continue
}
switch record.Resource.Name {
case "Nextcloud":
nextcloud = record
case "AdGuard Home":
adguard = record
}
}
if nextcloud == nil || adguard == nil {
t.Fatalf("expected TrueNAS app-container records for Nextcloud and AdGuard Home")
}
if nextcloud.ParentSourceID != "system:truenas-main" {
t.Fatalf("expected Nextcloud parent system:truenas-main, got %q", nextcloud.ParentSourceID)
}
if nextcloud.Resource.Technology != "docker" {
t.Fatalf("expected Nextcloud technology docker, got %q", nextcloud.Resource.Technology)
}
if nextcloud.Resource.Docker == nil {
t.Fatal("expected Nextcloud docker metadata")
}
if nextcloud.Resource.Docker.ContainerID != "nextcloud" {
t.Fatalf("expected Nextcloud canonical container ID %q, got %q", "nextcloud", nextcloud.Resource.Docker.ContainerID)
}
if nextcloud.Resource.Docker.Image != "docker.io/library/nextcloud:29.0.7" {
t.Fatalf("expected Nextcloud image %q, got %q", "docker.io/library/nextcloud:29.0.7", nextcloud.Resource.Docker.Image)
}
if nextcloud.Resource.Docker.Runtime != "docker" {
t.Fatalf("expected Nextcloud runtime docker, got %q", nextcloud.Resource.Docker.Runtime)
}
if nextcloud.Resource.TrueNAS == nil || nextcloud.Resource.TrueNAS.App == nil {
t.Fatal("expected Nextcloud native TrueNAS app metadata")
}
if nextcloud.Resource.TrueNAS.App.ID != "nextcloud" || nextcloud.Resource.TrueNAS.App.Name != "Nextcloud" {
t.Fatalf("unexpected Nextcloud native app identity: %+v", nextcloud.Resource.TrueNAS.App)
}
if nextcloud.Resource.TrueNAS.App.ContainerCount != 2 || len(nextcloud.Resource.TrueNAS.App.Containers) != 2 {
t.Fatalf("expected Nextcloud native app containers, got %+v", nextcloud.Resource.TrueNAS.App)
}
if len(nextcloud.Resource.TrueNAS.App.Images) != 2 {
t.Fatalf("expected Nextcloud native app images, got %+v", nextcloud.Resource.TrueNAS.App.Images)
}
if len(nextcloud.Resource.TrueNAS.App.Volumes) != 2 {
t.Fatalf("expected Nextcloud native app volumes, got %+v", nextcloud.Resource.TrueNAS.App.Volumes)
}
if len(nextcloud.Resource.Docker.Ports) != 1 || nextcloud.Resource.Docker.Ports[0].PublicPort != 30443 {
t.Fatalf("unexpected Nextcloud published ports: %+v", nextcloud.Resource.Docker.Ports)
}
if len(nextcloud.Resource.Docker.Mounts) != 2 {
t.Fatalf("expected Nextcloud mounts, got %+v", nextcloud.Resource.Docker.Mounts)
}
if nextcloud.Resource.Status != unifiedresources.StatusOnline {
t.Fatalf("expected Nextcloud status online, got %q", nextcloud.Resource.Status)
}
if adguard.Resource.Status != unifiedresources.StatusOffline {
t.Fatalf("expected AdGuard Home status offline, got %q", adguard.Resource.Status)
}
if adguard.Resource.Docker == nil || adguard.Resource.Docker.ContainerState != "exited" {
t.Fatalf("expected AdGuard Home container state exited, got %+v", adguard.Resource.Docker)
}
}
func TestRecordsIncludeTrueNASServicesOnSystemResource(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
provider := NewProvider(DefaultFixtures())
records := provider.Records()
if len(records) == 0 {
t.Fatal("expected fixture records from provider")
}
var system *unifiedresources.IngestRecord
for i := range records {
if records[i].Resource.Type == unifiedresources.ResourceTypeAgent && records[i].SourceID == "system:truenas-main" {
system = &records[i]
break
}
}
if system == nil {
t.Fatal("expected TrueNAS system record")
}
if system.Resource.TrueNAS == nil {
t.Fatal("expected TrueNAS system metadata")
}
services := system.Resource.TrueNAS.Services
if len(services) != 4 {
t.Fatalf("expected 4 TrueNAS services, got %+v", services)
}
if services[0].Service != "smb" || !services[0].Enabled || services[0].State != "RUNNING" || len(services[0].PIDs) != 2 {
t.Fatalf("unexpected SMB service metadata: %+v", services[0])
}
if services[3].Service != "smartd" || !services[3].Enabled || services[3].State != "STOPPED" {
t.Fatalf("expected stopped enabled smartd service, got %+v", services[3])
}
}
func TestRecordsIncludeTrueNASVMsAsCanonicalWorkloads(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
provider := NewProvider(DefaultFixtures())
records := provider.Records()
if len(records) == 0 {
t.Fatal("expected fixture records from provider")
}
var windows *unifiedresources.IngestRecord
var ubuntu *unifiedresources.IngestRecord
for i := range records {
record := &records[i]
if record.Resource.Type != unifiedresources.ResourceTypeVM {
continue
}
switch record.Resource.Name {
case "windows-lab":
windows = record
case "ubuntu-build":
ubuntu = record
}
}
if windows == nil || ubuntu == nil {
t.Fatalf("expected TrueNAS VM records for windows-lab and ubuntu-build")
}
if windows.ParentSourceID != "system:truenas-main" {
t.Fatalf("expected windows-lab parent system:truenas-main, got %q", windows.ParentSourceID)
}
if windows.Resource.TrueNAS == nil || windows.Resource.TrueNAS.VM == nil {
t.Fatal("expected windows-lab native TrueNAS VM metadata")
}
if windows.Resource.TrueNAS.VM.ID != "42" || windows.Resource.TrueNAS.VM.Name != "windows-lab" {
t.Fatalf("unexpected windows-lab native VM identity: %+v", windows.Resource.TrueNAS.VM)
}
if windows.Resource.TrueNAS.VM.VCPUs != 4 || windows.Resource.TrueNAS.VM.MemoryBytes != 8*1024*1024*1024 {
t.Fatalf("unexpected windows-lab native VM compute metadata: %+v", windows.Resource.TrueNAS.VM)
}
if !windows.Resource.TrueNAS.VM.Autostart || !windows.Resource.TrueNAS.VM.SecureBoot || !windows.Resource.TrueNAS.VM.TrustedPlatformModule {
t.Fatalf("expected windows-lab native VM flags, got %+v", windows.Resource.TrueNAS.VM)
}
if windows.Resource.Status != unifiedresources.StatusOnline {
t.Fatalf("expected windows-lab status online, got %q", windows.Resource.Status)
}
if ubuntu.Resource.Status != unifiedresources.StatusOffline {
t.Fatalf("expected ubuntu-build status offline, got %q", ubuntu.Resource.Status)
}
if ubuntu.Resource.TrueNAS == nil || ubuntu.Resource.TrueNAS.VM == nil || ubuntu.Resource.TrueNAS.VM.DomainState != "SHUTOFF" {
t.Fatalf("expected ubuntu-build native VM domain state, got %+v", ubuntu.Resource.TrueNAS)
}
}
func TestRecordsElevateOnlineDiskWhenTemperatureCritical(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
fixtures := DefaultFixtures()
fixtures.Disks = []Disk{{
ID: "disk-hot",
Name: "sda",
Pool: "tank",
Status: "ONLINE",
Model: "Seagate Exos X18",
Serial: "SER-HOT",
SizeBytes: 16 * 1024 * 1024 * 1024 * 1024,
Temperature: 72,
Transport: "sata",
Rotational: true,
}}
records := NewProvider(fixtures).Records()
if len(records) == 0 {
t.Fatal("expected fixture records from provider")
}
var diskRecord *unifiedresources.IngestRecord
for i := range records {
if records[i].Resource.Type == unifiedresources.ResourceTypePhysicalDisk {
diskRecord = &records[i]
break
}
}
if diskRecord == nil {
t.Fatal("expected physical disk record")
}
if diskRecord.Resource.Status != unifiedresources.StatusWarning {
t.Fatalf("expected hot disk status warning, got %s", diskRecord.Resource.Status)
}
if diskRecord.Resource.PhysicalDisk == nil || diskRecord.Resource.PhysicalDisk.Risk == nil {
t.Fatalf("expected hot disk physical risk, got %+v", diskRecord.Resource.PhysicalDisk)
}
if diskRecord.Resource.PhysicalDisk.Risk.Level != storagehealth.RiskCritical {
t.Fatalf("expected hot disk critical risk, got %+v", diskRecord.Resource.PhysicalDisk.Risk)
}
}
func TestRecordsProjectUnavailableDiskTelemetryAsUnknownWithoutRisk(t *testing.T) {
for _, status := range []string{"", "UNKNOWN", "UNAVAILABLE", "N/A", "SMART unavailable"} {
t.Run(status, func(t *testing.T) {
record := truenasDiskRecordForStatus(t, status)
if record.Resource.Status != unifiedresources.StatusUnknown {
t.Fatalf("status %q projected resource status %q, want %q", status, record.Resource.Status, unifiedresources.StatusUnknown)
}
if record.Resource.PhysicalDisk == nil {
t.Fatal("expected physical disk metadata")
}
if record.Resource.PhysicalDisk.Health != "UNKNOWN" {
t.Fatalf("status %q projected health %q, want UNKNOWN", status, record.Resource.PhysicalDisk.Health)
}
if record.Resource.PhysicalDisk.Risk != nil {
t.Fatalf("status %q should not project replacement risk, got %+v", status, record.Resource.PhysicalDisk.Risk)
}
if len(record.Resource.Incidents) != 0 {
t.Fatalf("status %q should not project incidents, got %+v", status, record.Resource.Incidents)
}
})
}
}
func TestRecordsProjectExplicitUnavailableSMARTAsUnknownWithoutRisk(t *testing.T) {
record := truenasDiskRecordForDisk(t, Disk{
ID: "disk-sda",
Name: "sda",
Pool: "tank",
Status: "ONLINE",
Health: "UNAVAILABLE",
HealthStatusPresent: true,
Model: "Generic SATA",
Serial: "SER-SDA",
SizeBytes: 1_000_000,
Transport: "sata",
Rotational: true,
Temperature: 34,
})
if record.Resource.Status != unifiedresources.StatusUnknown {
t.Fatalf("projected resource status %q, want %q", record.Resource.Status, unifiedresources.StatusUnknown)
}
if record.Resource.PhysicalDisk == nil {
t.Fatal("expected physical disk metadata")
}
if record.Resource.PhysicalDisk.Health != "UNKNOWN" {
t.Fatalf("projected health %q, want UNKNOWN", record.Resource.PhysicalDisk.Health)
}
if record.Resource.PhysicalDisk.Risk != nil {
t.Fatalf("unavailable SMART should not project replacement risk, got %+v", record.Resource.PhysicalDisk.Risk)
}
if len(record.Resource.Incidents) != 0 {
t.Fatalf("unavailable SMART should not project incidents, got %+v", record.Resource.Incidents)
}
}
func TestRecordsProjectExplicitFailedSMARTAsReplacementRisk(t *testing.T) {
record := truenasDiskRecordForDisk(t, Disk{
ID: "disk-sda",
Name: "sda",
Pool: "tank",
Status: "ONLINE",
Health: "FAILED",
HealthStatusPresent: true,
Model: "Generic SATA",
Serial: "SER-SDA",
SizeBytes: 1_000_000,
Transport: "sata",
Rotational: true,
Temperature: 34,
})
if record.Resource.Status != unifiedresources.StatusWarning {
t.Fatalf("projected resource status %q, want %q", record.Resource.Status, unifiedresources.StatusWarning)
}
if record.Resource.PhysicalDisk == nil {
t.Fatal("expected physical disk metadata")
}
if record.Resource.PhysicalDisk.Health != "FAILED" {
t.Fatalf("projected health %q, want FAILED", record.Resource.PhysicalDisk.Health)
}
if record.Resource.PhysicalDisk.Risk == nil {
t.Fatal("explicit failed SMART should project replacement risk")
}
if !containsRiskReason(record.Resource.PhysicalDisk.Risk.Reasons, "health_status") {
t.Fatalf("missing health_status risk, got %+v", record.Resource.PhysicalDisk.Risk.Reasons)
}
}
func TestRecordsPreservePassedDegradedAndFailedDiskProjection(t *testing.T) {
tests := []struct {
name string
status string
wantHealth string
wantStatus unifiedresources.ResourceStatus
wantRiskLevel storagehealth.RiskLevel
wantRiskReason string
wantRiskNil bool
}{
{
name: "passed",
status: "ONLINE",
wantHealth: "PASSED",
wantStatus: unifiedresources.StatusOnline,
wantRiskNil: true,
},
{
name: "degraded",
status: "DEGRADED",
wantHealth: "DEGRADED",
wantStatus: unifiedresources.StatusWarning,
wantRiskLevel: storagehealth.RiskWarning,
wantRiskReason: "truenas_disk_state",
},
{
name: "failed",
status: "FAILED",
wantHealth: "FAILED",
wantStatus: unifiedresources.StatusWarning,
wantRiskLevel: storagehealth.RiskCritical,
wantRiskReason: "truenas_disk_state",
},
{
name: "faulted",
status: "FAULTED",
wantHealth: "FAILED",
wantStatus: unifiedresources.StatusWarning,
wantRiskLevel: storagehealth.RiskCritical,
wantRiskReason: "truenas_disk_state",
},
}
for _, tt := range tests {
t.Run(tt.name, func(t *testing.T) {
record := truenasDiskRecordForStatus(t, tt.status)
if record.Resource.Status != tt.wantStatus {
t.Fatalf("status %q projected resource status %q, want %q", tt.status, record.Resource.Status, tt.wantStatus)
}
if record.Resource.PhysicalDisk == nil {
t.Fatal("expected physical disk metadata")
}
if record.Resource.PhysicalDisk.Health != tt.wantHealth {
t.Fatalf("status %q projected health %q, want %q", tt.status, record.Resource.PhysicalDisk.Health, tt.wantHealth)
}
if tt.wantRiskNil {
if record.Resource.PhysicalDisk.Risk != nil {
t.Fatalf("status %q should not project risk, got %+v", tt.status, record.Resource.PhysicalDisk.Risk)
}
return
}
if record.Resource.PhysicalDisk.Risk == nil {
t.Fatalf("status %q should project risk", tt.status)
}
if record.Resource.PhysicalDisk.Risk.Level != tt.wantRiskLevel {
t.Fatalf("status %q projected risk level %q, want %q", tt.status, record.Resource.PhysicalDisk.Risk.Level, tt.wantRiskLevel)
}
if !containsRiskReason(record.Resource.PhysicalDisk.Risk.Reasons, tt.wantRiskReason) {
t.Fatalf("status %q missing risk reason %q, got %+v", tt.status, tt.wantRiskReason, record.Resource.PhysicalDisk.Risk.Reasons)
}
})
}
}
func TestRecordsProjectDiskTemperatureAggregatesIntoCanonicalMetadata(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(true)
t.Cleanup(func() {
SetFeatureEnabled(previous)
})
provider := NewProvider(DefaultFixtures())
records := provider.Records()
if len(records) == 0 {
t.Fatal("expected fixture records from provider")
}
for _, record := range records {
if record.Resource.Type != unifiedresources.ResourceTypePhysicalDisk || record.Resource.Name != "sda" {
continue
}
if record.Resource.PhysicalDisk == nil {
t.Fatal("expected canonical physical-disk metadata")
}
aggregate := record.Resource.PhysicalDisk.TemperatureAggregate
if aggregate == nil {
t.Fatalf("expected temperature aggregate on canonical physical disk: %+v", record.Resource.PhysicalDisk)
}
if aggregate.WindowDays != 7 || aggregate.MinCelsius != 29.0 || aggregate.AvgCelsius != 32.7 || aggregate.MaxCelsius != 38.0 {
t.Fatalf("unexpected canonical disk temperature aggregate: %+v", aggregate)
}
return
}
t.Fatal("expected sda physical disk record")
}
func truenasDiskRecordForStatus(t *testing.T, status string) unifiedresources.IngestRecord {
t.Helper()
return truenasDiskRecordForDisk(t, Disk{
ID: "disk-sda",
Name: "sda",
Pool: "tank",
Status: status,
Model: "Generic SATA",
Serial: "SER-SDA",
SizeBytes: 1_000_000,
Transport: "sata",
Rotational: true,
Temperature: 34,
})
}
func truenasDiskRecordForDisk(t *testing.T, disk Disk) unifiedresources.IngestRecord {
t.Helper()
records := FixtureRecords(FixtureSnapshot{
CollectedAt: time.Date(2026, 5, 28, 12, 0, 0, 0, time.UTC),
System: SystemInfo{
Hostname: "truenas-test",
Healthy: true,
},
Disks: []Disk{disk},
})
for _, record := range records {
if record.Resource.Type == unifiedresources.ResourceTypePhysicalDisk {
return record
}
}
t.Fatal("expected physical disk record")
return unifiedresources.IngestRecord{}
}
func TestProviderPhysicalDiskTemperatureHistoryUsesCanonicalMetricIDs(t *testing.T) {
fixtures := DefaultFixtures()
now := time.Date(2026, 3, 29, 20, 0, 0, 0, time.UTC)
fetcher := &controllableStubFetcher{
snapshot: &fixtures,
diskHistory: map[string][]TimeSeriesPoint{
"sda": {
{Timestamp: now.Add(-2 * time.Hour), Value: 30},
{Timestamp: now.Add(-1 * time.Hour), Value: 32},
{Timestamp: now, Value: 34},
},
},
}
provider := NewLiveProvider(fetcher)
if err := provider.Refresh(context.Background()); err != nil {
t.Fatalf("Refresh() error = %v", err)
}
history, err := provider.PhysicalDiskTemperatureHistory(context.Background(), 4*time.Hour)
if err != nil {
t.Fatalf("PhysicalDiskTemperatureHistory() error = %v", err)
}
points, ok := history["ZL0A1234"]
if !ok {
t.Fatalf("expected canonical disk metric id ZL0A1234, got keys %#v", history)
}
if len(points) != 3 {
t.Fatalf("expected 3 temperature history points, got %+v", points)
}
if points[len(points)-1].Value != 34 {
t.Fatalf("expected latest point value 34, got %+v", points)
}
}
func TestProviderSystemMetricHistoryUsesCanonicalAgentMetricIDs(t *testing.T) {
fixtures := DefaultFixtures()
now := time.Date(2026, 3, 29, 20, 0, 0, 0, time.UTC)
fetcher := &controllableStubFetcher{
snapshot: &fixtures,
systemHistory: &SystemMetricHistory{
CPUPercent: []TimeSeriesPoint{
{Timestamp: now.Add(-2 * time.Hour), Value: 20},
{Timestamp: now, Value: 34},
},
MemoryUsedBytes: []TimeSeriesPoint{
{Timestamp: now.Add(-2 * time.Hour), Value: 8 * 1024 * 1024 * 1024},
{Timestamp: now, Value: 10 * 1024 * 1024 * 1024},
},
NetInRate: []TimeSeriesPoint{
{Timestamp: now, Value: 4096},
},
},
}
provider := NewLiveProvider(fetcher)
if err := provider.Refresh(context.Background()); err != nil {
t.Fatalf("Refresh() error = %v", err)
}
resourceID, history, err := provider.SystemMetricHistory(context.Background(), 4*time.Hour)
if err != nil {
t.Fatalf("SystemMetricHistory() error = %v", err)
}
if resourceID != "truenas-main" {
t.Fatalf("resourceID = %q, want truenas-main", resourceID)
}
if got := len(history["cpu"]); got != 2 {
t.Fatalf("expected cpu history points, got %+v", history)
}
if got := len(history["memory"]); got != 2 {
t.Fatalf("expected memory history points, got %+v", history)
}
if history["memory"][1].Value <= 0 {
t.Fatalf("expected memory percent history, got %+v", history["memory"])
}
if got := len(history["netin"]); got != 1 {
t.Fatalf("expected netin history points, got %+v", history)
}
}