Make fixture graph the only mock runtime API

This commit is contained in:
rcourtman 2026-03-31 15:42:16 +01:00
parent cbfc7ce750
commit 9187ee727f
24 changed files with 223 additions and 111 deletions

View file

@ -271,6 +271,11 @@ for API-backed platform onboarding. TrueNAS and VMware connection-list payloads
shown in Platform connections must be assembled from the canonical
`internal/mock/` platform fixture layer, so settings handoff metadata cannot
drift from the runtime mock inventory and shared storage/recovery context.
That same lifecycle-adjacent mock path must stay graph-first at the shared
`internal/api/` boundary. When lifecycle-adjacent handlers depend on mock
platform inventory or recovery context, they must consume
`internal/mock/fixture_graph.go` and its graph-owned projections instead of
reintroducing snapshot-only or platform-only helper exports.
That same hosted continuity contract also applies to the older direct tenant
magic-link path. Lifecycle-adjacent control-plane redirects through
`/auth/cloud-handoff` must preserve canonical account/user/role identity in the

View file

@ -205,6 +205,7 @@ Own canonical runtime payload shapes between backend and frontend.
2. Update frontend API types in the same slice
3. Route runtime changes through the explicit API-contract proof policies in `registry.json`; default fallback proof routing is not allowed
4. Update this contract when canonical payload ownership changes
5. Keep mock recovery handlers graph-first: `/api/recovery/rollups`, `/api/recovery/points`, `/api/recovery/series`, and `/api/recovery/facets` must derive mock data from `internal/mock/fixture_graph.go` and its graph-owned `RecoveryPoints()` projection instead of route-local mock caches or legacy helper exports
5. Keep `/api/resources` policy metadata aligned across backend payload tests and canonical frontend resource consumers whenever sensitivity or routing fields change
6. Keep Patrol status payloads explicit enough that the frontend can present blocked runtime state without treating a previously healthy summary snapshot as current runtime truth, and keep Patrol recency semantics explicit in transport by reserving `last_patrol_at` for completed full patrols while exposing any Patrol activity separately through `last_activity_at`
and the scoped-trigger status payload on that same Patrol status surface, so queued scoped work, busy-mode state, and per-source enablement (`alert` versus `anomaly`) stay transport-backed instead of being inferred by page-local heuristics

View file

@ -376,9 +376,10 @@ That same fixture authority now also includes legacy snapshot-backed platforms.
`internal/mock/fixture_graph.go` runtime graph as the one mock owner for
legacy Proxmox/Docker/Kubernetes/agent/PBS/PMG snapshot state plus
provider-backed TrueNAS and VMware fixtures. Monitoring must not rebuild mock
provider context from standalone defaults or mix a legacy `GenerateMockData`
snapshot with separate provider fixtures when seeding read-state or metrics
history.
provider context from standalone defaults, consume partial legacy helper
exports, or mix snapshot state with separate provider fixtures when seeding
read-state or metrics history. The graph and its methods are the canonical
mock runtime API.
That same boundary now also owns native disk-history fallback when Pulse's own
history is shallow. `internal/truenas/client.go`,
`internal/truenas/provider.go`, `internal/monitoring/truenas_poller.go`, and

View file

@ -154,7 +154,7 @@ querying, and the operator-facing storage health presentation layer.
come from the canonical `internal/mock/fixture_graph.go` owner so legacy
snapshot-backed platforms, provider-backed fixtures, unified inventory,
and recovery/storage context stay aligned instead of drifting through
recovery-local fixture assembly.
recovery-local fixture assembly or partial mock helper APIs.
## Current State

View file

@ -270,6 +270,9 @@ one graph instead of combining a legacy snapshot read with standalone provider
defaults. The shared resource graph must therefore see one coherent mock
platform set regardless of whether a platform is snapshot-backed or
supplemental-provider-backed.
Callers should therefore consume `CurrentFixtureGraph()` and graph-owned
projections rather than reintroducing platform-only or state-only mock helper
exports.
TrueNAS-managed applications now follow the same canonical workload rule. One
TrueNAS app instance from `app.query` must project as one canonical
`app-container` resource under `SourceTrueNAS`, reusing the shared workload and

View file

@ -3146,6 +3146,56 @@ func TestContract_RecoveryPointPayloadUsesCanonicalPlatformField(t *testing.T) {
assertJSONSnapshot(t, got, want)
}
func TestContract_RecoveryPointsMockPathReturnsCanonicalProviderBackedFixtures(t *testing.T) {
previousEnabled := mock.IsMockEnabled()
previousConfig := mock.GetConfig()
t.Cleanup(func() {
mock.SetEnabled(false)
mock.SetMockConfig(previousConfig)
if previousEnabled {
mock.SetEnabled(true)
mock.SetMockConfig(previousConfig)
}
})
t.Setenv("PULSE_MOCK_NODES", "1")
t.Setenv("PULSE_MOCK_VMS_PER_NODE", "0")
t.Setenv("PULSE_MOCK_LXCS_PER_NODE", "0")
t.Setenv("PULSE_MOCK_DOCKER_HOSTS", "0")
t.Setenv("PULSE_MOCK_DOCKER_CONTAINERS", "0")
t.Setenv("PULSE_MOCK_GENERIC_HOSTS", "0")
t.Setenv("PULSE_MOCK_K8S_CLUSTERS", "0")
t.Setenv("PULSE_MOCK_K8S_NODES", "0")
t.Setenv("PULSE_MOCK_K8S_PODS", "0")
t.Setenv("PULSE_MOCK_K8S_DEPLOYMENTS", "0")
mock.SetEnabled(false)
mock.SetEnabled(true)
req := httptest.NewRequest(http.MethodGet, "/api/recovery/points?platform=truenas&limit=10", nil)
rec := httptest.NewRecorder()
NewRecoveryHandlers(nil).HandleListPoints(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var resp recoveryPointsResponse
if err := json.Unmarshal(rec.Body.Bytes(), &resp); err != nil {
t.Fatalf("unmarshal recovery points response: %v", err)
}
if len(resp.Data) == 0 {
t.Fatal("expected provider-backed mock recovery points in response")
}
if resp.Data[0].Platform != recovery.Provider("truenas") {
t.Fatalf("platform = %q, want %q", resp.Data[0].Platform, "truenas")
}
if resp.Data[0].Display == nil {
t.Fatal("expected normalized recovery display payload on mock points response")
}
}
func TestContract_RecoveryRollupPayloadUsesCanonicalPlatformsField(t *testing.T) {
payload := buildRecoveryRollupPayload(recovery.ProtectionRollup{
RollupID: "rollup-1",

View file

@ -91,7 +91,7 @@ func TestMetricsHistoryFallbackMockDiskSynthesizesSeries(t *testing.T) {
mock.SetEnabled(true)
t.Cleanup(func() { mock.SetEnabled(false) })
state := mock.GetMockState()
state := mock.CurrentFixtureGraph().State
var disk models.PhysicalDisk
found := false
for _, candidate := range state.PhysicalDisks {

View file

@ -225,7 +225,7 @@ func (h *RecoveryHandlers) HandleListPoints(w http.ResponseWriter, r *http.Reque
)
if mock.IsMockEnabled() {
all := mock.GetMockRecoveryPoints()
all := mock.CurrentFixtureGraph().RecoveryPoints()
filtered := filterRecoveryPoints(all, opts)
total = len(filtered)
points = paginateRecoveryPoints(filtered, opts.Page, opts.Limit)
@ -336,7 +336,7 @@ func (h *RecoveryHandlers) HandleListSeries(w http.ResponseWriter, r *http.Reque
var series []recovery.PointsSeriesBucket
if mock.IsMockEnabled() {
all := mock.GetMockRecoveryPoints()
all := mock.CurrentFixtureGraph().RecoveryPoints()
filtered := filterRecoveryPoints(all, opts)
// Count only completed points.
series = buildSeriesFromPoints(filtered, opts, tzOffsetMin)
@ -402,7 +402,7 @@ func (h *RecoveryHandlers) HandleListFacets(w http.ResponseWriter, r *http.Reque
var facets recovery.PointsFacets
if mock.IsMockEnabled() {
all := mock.GetMockRecoveryPoints()
all := mock.CurrentFixtureGraph().RecoveryPoints()
filtered := filterRecoveryPoints(all, opts)
facets = buildFacetsFromPoints(filtered)
} else {
@ -670,7 +670,7 @@ func (h *RecoveryHandlers) HandleListRollups(w http.ResponseWriter, r *http.Requ
)
if mock.IsMockEnabled() {
all := mock.GetMockRecoveryPoints()
all := mock.CurrentFixtureGraph().RecoveryPoints()
filtered := filterRecoveryPointsForRollups(all, opts)
rollups = recovery.BuildRollupsFromPoints(filtered)
total = len(rollups)

View file

@ -8,8 +8,8 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/models"
)
// GenerateAlertHistory generates historical alert data for testing
func GenerateAlertHistory(nodes []models.Node, vms []models.VM, containers []models.Container) []models.Alert {
// buildAlertHistory derives historical alert data from the canonical fixture graph.
func buildAlertHistory(nodes []models.Node, vms []models.VM, containers []models.Container) []models.Alert {
var history []models.Alert
// Alert types and messages

View file

@ -0,0 +1,40 @@
package mock
import (
"os"
"strings"
"testing"
)
func TestMockPackageDoesNotReintroduceLegacySnapshotExports(t *testing.T) {
t.Helper()
files := []string{
"generator.go",
"alert_history.go",
"integration.go",
"platform_fixtures.go",
"recovery_points.go",
}
forbiddenSnippets := []string{
"func GenerateMockData(",
"func GenerateAlertHistory(",
"func GetMockState(",
"func GetMockRecoveryPoints(",
"func GetPlatformFixtures(",
"func DefaultPlatformFixtures(",
}
for _, path := range files {
data, err := os.ReadFile(path)
if err != nil {
t.Fatalf("failed to read %s: %v", path, err)
}
source := string(data)
for _, snippet := range forbiddenSnippets {
if strings.Contains(source, snippet) {
t.Fatalf("%s must not reintroduce legacy mock export %q", path, snippet)
}
}
}
}

View file

@ -65,13 +65,13 @@ func TestSetMockConfigNormalizesInvalidValues(t *testing.T) {
}
}
func TestGenerateMockDataAllowsZeroGuestConfig(t *testing.T) {
func TestBuildFixtureStateAllowsZeroGuestConfig(t *testing.T) {
cfg := DefaultConfig
cfg.NodeCount = 3
cfg.VMsPerNode = 0
cfg.LXCsPerNode = 0
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
if len(data.Nodes) != cfg.NodeCount {
t.Fatalf("expected %d nodes, got %d", cfg.NodeCount, len(data.Nodes))

View file

@ -25,13 +25,13 @@ func emptyFixtureGraph() FixtureGraph {
}
func buildFixtureGraph(cfg MockConfig, now time.Time) FixtureGraph {
state := GenerateMockData(cfg)
state := buildFixtureState(cfg)
state.LastUpdate = now
return FixtureGraph{
State: state,
AlertHistory: GenerateAlertHistory(state.Nodes, state.VMs, state.Containers),
PlatformFixtures: DefaultPlatformFixtures(),
AlertHistory: buildAlertHistory(state.Nodes, state.VMs, state.Containers),
PlatformFixtures: defaultPlatformFixtures(),
}
}
@ -47,7 +47,7 @@ func (g *FixtureGraph) UpdateMetrics(cfg MockConfig, now time.Time) {
if g == nil {
return
}
UpdateMetrics(&g.State, cfg)
updateFixtureStateMetrics(&g.State, cfg)
g.State.LastUpdate = now
}
@ -89,15 +89,12 @@ func CurrentFixtureGraph() FixtureGraph {
return cloneFixtureGraph(mockGraph)
}
func GetPlatformFixtures() PlatformFixtures {
func currentOrDefaultPlatformFixtures() PlatformFixtures {
if !IsMockEnabled() {
return DefaultPlatformFixtures()
return defaultPlatformFixtures()
}
dataMu.RLock()
defer dataMu.RUnlock()
return clonePlatformFixtures(mockGraph.PlatformFixtures)
return CurrentFixtureGraph().PlatformFixtures
}
func clonePlatformFixtures(in PlatformFixtures) PlatformFixtures {

View file

@ -328,8 +328,9 @@ func generateVirtualDisks() ([]models.Disk, models.Disk) {
return disks, aggregated
}
// GenerateMockData creates a simulated state snapshot for demo/test environments.
func GenerateMockData(config MockConfig) models.StateSnapshot {
// buildFixtureState synthesizes the snapshot-backed portion of the canonical
// fixture graph for demo and test environments.
func buildFixtureState(config MockConfig) models.StateSnapshot {
// rand is automatically seeded in Go 1.20+
config = normalizeMockConfig(config)
@ -4799,7 +4800,7 @@ func generateSnapshots(vms []models.VM, containers []models.Container) []models.
}
// UpdateMetrics simulates changing metrics over time
func UpdateMetrics(data *models.StateSnapshot, config MockConfig) {
func updateFixtureStateMetrics(data *models.StateSnapshot, config MockConfig) {
updateDockerHosts(data, config)
updateKubernetesClusters(data, config)
updateHosts(data, config)

View file

@ -7,12 +7,12 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/models"
)
func TestGenerateMockDataIncludesDockerHosts(t *testing.T) {
func TestBuildFixtureStateIncludesDockerHosts(t *testing.T) {
cfg := DefaultConfig
cfg.DockerHostCount = 2
cfg.DockerContainersPerHost = 5
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
if len(data.DockerHosts) != cfg.DockerHostCount {
t.Fatalf("expected %d docker hosts, got %d", cfg.DockerHostCount, len(data.DockerHosts))
@ -43,7 +43,7 @@ func TestComputeGuestCountsHandlesZeroBaselines(t *testing.T) {
}
}
func TestGenerateMockDataWithZeroGuestBaselinesDoesNotPanic(t *testing.T) {
func TestBuildFixtureStateWithZeroGuestBaselinesDoesNotPanic(t *testing.T) {
cfg := DefaultConfig
cfg.NodeCount = 4
cfg.VMsPerNode = 0
@ -56,21 +56,21 @@ func TestGenerateMockDataWithZeroGuestBaselinesDoesNotPanic(t *testing.T) {
func() {
defer func() {
if r := recover(); r != nil {
t.Fatalf("GenerateMockData panicked on iteration %d: %v", i, r)
t.Fatalf("buildFixtureState panicked on iteration %d: %v", i, r)
}
}()
_ = GenerateMockData(cfg)
_ = buildFixtureState(cfg)
}()
}
}
func TestGenerateMockDataIncludesSwarmServices(t *testing.T) {
func TestBuildFixtureStateIncludesSwarmServices(t *testing.T) {
cfg := DefaultConfig
cfg.DockerHostCount = 4
cfg.DockerContainersPerHost = 6
cfg.RandomMetrics = false
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
found := false
for _, host := range data.DockerHosts {
@ -92,12 +92,12 @@ func TestGenerateMockDataIncludesSwarmServices(t *testing.T) {
}
}
func TestGenerateMockDataIncludesHostAgents(t *testing.T) {
func TestBuildFixtureStateIncludesHostAgents(t *testing.T) {
cfg := DefaultConfig
cfg.GenericHostCount = 5
cfg.RandomMetrics = false
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
expectedMin := cfg.GenericHostCount
if cfg.NodeCount > expectedMin {
@ -120,13 +120,13 @@ func TestGenerateMockDataIncludesHostAgents(t *testing.T) {
}
}
func TestGenerateMockDataLinksAllNodesToHostAgents(t *testing.T) {
func TestBuildFixtureStateLinksAllNodesToHostAgents(t *testing.T) {
cfg := DefaultConfig
cfg.NodeCount = 7
cfg.GenericHostCount = 2
cfg.RandomMetrics = false
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
if len(data.Hosts) < cfg.NodeCount {
t.Fatalf("expected enough hosts to link all nodes, got hosts=%d nodes=%d", len(data.Hosts), cfg.NodeCount)
@ -151,13 +151,13 @@ func TestGenerateMockDataLinksAllNodesToHostAgents(t *testing.T) {
}
}
func TestGenerateMockDataPopulatesHostIORates(t *testing.T) {
func TestBuildFixtureStatePopulatesHostIORates(t *testing.T) {
cfg := DefaultConfig
cfg.NodeCount = 6
cfg.GenericHostCount = 1
cfg.RandomMetrics = true
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
for _, host := range data.Hosts {
if host.Status == "offline" {
@ -175,12 +175,12 @@ func TestGenerateMockDataPopulatesHostIORates(t *testing.T) {
}
}
func TestGenerateMockDataPopulatesDockerHostIORates(t *testing.T) {
func TestBuildFixtureStatePopulatesDockerHostIORates(t *testing.T) {
cfg := DefaultConfig
cfg.DockerHostCount = 3
cfg.RandomMetrics = true
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
if len(data.DockerHosts) == 0 {
t.Fatal("expected docker hosts in mock data")
@ -208,14 +208,14 @@ func TestGenerateMockDataPopulatesDockerHostIORates(t *testing.T) {
}
}
func TestGenerateMockDataPopulatesKubernetesUsageMetrics(t *testing.T) {
func TestBuildFixtureStatePopulatesKubernetesUsageMetrics(t *testing.T) {
cfg := DefaultConfig
cfg.K8sClusterCount = 1
cfg.K8sNodesPerCluster = 4
cfg.K8sPodsPerCluster = 24
cfg.RandomMetrics = false
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
if len(data.KubernetesClusters) != 1 {
t.Fatalf("expected exactly one kubernetes cluster, got %d", len(data.KubernetesClusters))
}
@ -269,7 +269,7 @@ func TestGenerateMockDataPopulatesKubernetesUsageMetrics(t *testing.T) {
}
}
func TestGenerateMockDataCreatesHostEntriesForKubernetesNodes(t *testing.T) {
func TestBuildFixtureStateCreatesHostEntriesForKubernetesNodes(t *testing.T) {
cfg := DefaultConfig
cfg.K8sClusterCount = 1
cfg.K8sNodesPerCluster = 3
@ -277,7 +277,7 @@ func TestGenerateMockDataCreatesHostEntriesForKubernetesNodes(t *testing.T) {
cfg.NodeCount = 0
cfg.RandomMetrics = false
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
if len(data.KubernetesClusters) == 0 {
t.Fatal("expected kubernetes clusters in mock data")
}
@ -310,7 +310,7 @@ func TestMockStateIncludesHostAgents(t *testing.T) {
SetEnabled(false)
})
state := GetMockState()
state := CurrentFixtureGraph().State
if len(state.Hosts) == 0 {
t.Fatalf("expected hosts in mock state, got %d", len(state.Hosts))
}
@ -329,8 +329,8 @@ func TestUpdateMetricsMaintainsServiceHealth(t *testing.T) {
cfg.DockerHostCount = 3
cfg.DockerContainersPerHost = 6
data := GenerateMockData(cfg)
UpdateMetrics(&data, cfg)
data := buildFixtureState(cfg)
updateFixtureStateMetrics(&data, cfg)
for _, host := range data.DockerHosts {
if len(host.Services) == 0 {
@ -354,10 +354,10 @@ func TestUpdateMetricsMaintainsServiceHealth(t *testing.T) {
}
}
func TestGenerateMockDataIncludesPMGInstances(t *testing.T) {
func TestBuildFixtureStateIncludesPMGInstances(t *testing.T) {
cfg := DefaultConfig
data := GenerateMockData(cfg)
data := buildFixtureState(cfg)
if len(data.PMGInstances) == 0 {
t.Fatalf("expected PMG instances in mock data")
@ -432,16 +432,16 @@ func TestCloneStatePreservesIgnoredInfrastructureEntries(t *testing.T) {
}
}
func TestGenerateMockDataInitializesIgnoredInfrastructureSlices(t *testing.T) {
data := GenerateMockData(DefaultConfig)
func TestBuildFixtureStateInitializesIgnoredInfrastructureSlices(t *testing.T) {
data := buildFixtureState(DefaultConfig)
if data.RemovedDockerHosts == nil {
t.Fatal("expected GenerateMockData to initialize RemovedDockerHosts")
t.Fatal("expected buildFixtureState to initialize RemovedDockerHosts")
}
if data.RemovedHostAgents == nil {
t.Fatal("expected GenerateMockData to initialize RemovedHostAgents")
t.Fatal("expected buildFixtureState to initialize RemovedHostAgents")
}
if data.RemovedKubernetesClusters == nil {
t.Fatal("expected GenerateMockData to initialize RemovedKubernetesClusters")
t.Fatal("expected buildFixtureState to initialize RemovedKubernetesClusters")
}
}

View file

@ -428,18 +428,6 @@ func SetMockConfig(cfg MockConfig) {
Msg("Mock configuration updated")
}
// GetMockState returns the current mock state snapshot.
func GetMockState() models.StateSnapshot {
if !IsMockEnabled() {
return models.EmptyStateSnapshot()
}
dataMu.RLock()
defer dataMu.RUnlock()
return cloneState(mockGraph.State)
}
// UpdateAlertSnapshots replaces the active and recently resolved alert lists used for mock mode.
// This lets other components read alert data without querying the live alert manager, which can
// be locked while alerts are being generated. Keeping a snapshot here prevents any blocking when

View file

@ -51,7 +51,7 @@ type VMwareConnectionFixture struct {
VIRelease string
}
func DefaultPlatformFixtures() PlatformFixtures {
func defaultPlatformFixtures() PlatformFixtures {
return PlatformFixtures{
TrueNAS: truenas.DefaultFixtures(),
VMware: vmware.DefaultFixtures(),
@ -59,11 +59,11 @@ func DefaultPlatformFixtures() PlatformFixtures {
}
func DefaultTrueNASConnectionFixture() TrueNASConnectionFixture {
return defaultTrueNASConnectionFixture(GetPlatformFixtures())
return defaultTrueNASConnectionFixture(currentOrDefaultPlatformFixtures())
}
func DefaultVMwareConnectionFixture() VMwareConnectionFixture {
fixtures := GetPlatformFixtures().VMware
fixtures := currentOrDefaultPlatformFixtures().VMware
return VMwareConnectionFixture{
ID: strings.TrimSpace(fixtures.ConnectionID),
@ -108,7 +108,7 @@ func defaultTrueNASConnectionFixture(fixtures PlatformFixtures) TrueNASConnectio
}
func SupplementalRecords(source unifiedresources.DataSource) []unifiedresources.IngestRecord {
fixtures := GetPlatformFixtures()
fixtures := currentOrDefaultPlatformFixtures()
switch normalizePlatformSource(source) {
case unifiedresources.SourceTrueNAS:
return truenas.FixtureRecords(fixtures.TrueNAS)

View file

@ -11,14 +11,6 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/recovery"
)
func GetMockRecoveryPoints() []recovery.RecoveryPoint {
if !IsMockEnabled() {
return nil
}
return CurrentFixtureGraph().RecoveryPoints()
}
func (g FixtureGraph) RecoveryPoints() []recovery.RecoveryPoint {
return generateMockRecoveryPoints(g.State, g.PlatformFixtures)
}

View file

@ -40,7 +40,7 @@ func TestCurrentFixtureGraphReturnsDefensiveCopies(t *testing.T) {
}
}
func TestGetMockRecoveryPointsDerivesSubjectsFromCurrentGraph(t *testing.T) {
func TestFixtureGraphRecoveryPointsDeriveSubjectsFromCurrentGraph(t *testing.T) {
previous := IsMockEnabled()
SetEnabled(true)
t.Cleanup(func() { SetEnabled(previous) })
@ -82,7 +82,7 @@ func TestGetMockRecoveryPointsDerivesSubjectsFromCurrentGraph(t *testing.T) {
}
}
points := GetMockRecoveryPoints()
points := CurrentFixtureGraph().RecoveryPoints()
if len(points) == 0 {
t.Fatal("expected mock recovery points")
}

View file

@ -72,6 +72,22 @@ func TestNoGetStateResourceArrayRegression(t *testing.T) {
}
}
func TestMonitoringRuntimeAvoidsLegacyMockPartialHelpers(t *testing.T) {
forbiddenSnippets := []string{
"mock.GetMockState(",
"mock.GetPlatformFixtures(",
"mock.GetMockRecoveryPoints(",
}
for name, content := range readMonitoringRuntimeFiles(t) {
for _, snippet := range forbiddenSnippets {
if strings.Contains(content, snippet) {
t.Fatalf("%s must not depend on legacy mock partial helper %q", name, snippet)
}
}
}
}
func TestConnectedInfrastructureUsesSharedTopLevelSystemResolver(t *testing.T) {
data, err := os.ReadFile("connected_infrastructure.go")
if err != nil {

View file

@ -479,10 +479,11 @@ func buildTieredTimestamps(now time.Time, totalDuration time.Duration) []time.Ti
return timestamps
}
func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.StateSnapshot, now time.Time, seedDuration, interval time.Duration) {
func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, graph mock.FixtureGraph, now time.Time, seedDuration, interval time.Duration) {
if mh == nil {
return
}
state := graph.State
if seedDuration <= 0 || interval <= 0 {
return
}
@ -875,7 +876,7 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.
time.Sleep(50 * time.Millisecond)
}
platformFixtures := mock.GetPlatformFixtures()
platformFixtures := graph.PlatformFixtures
trueNASFixtures := platformFixtures.TrueNAS
log.Debug().Int("pools", len(trueNASFixtures.Pools)).Int("datasets", len(trueNASFixtures.Datasets)).Msg("mock seeding: processing TrueNAS fixtures")
@ -1006,24 +1007,24 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.
}
// recordTrueNASFixturesMetrics records disk usage metrics for TrueNAS pools and datasets.
func recordTrueNASFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, ts time.Time) {
fixtures := mock.GetPlatformFixtures().TrueNAS
func recordTrueNASFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, fixtures mock.PlatformFixtures, ts time.Time) {
snapshot := fixtures.TrueNAS
totalCap, totalUsed := int64(0), int64(0)
for _, pool := range fixtures.Pools {
for _, pool := range snapshot.Pools {
totalCap += pool.TotalBytes
totalUsed += pool.UsedBytes
}
if totalCap > 0 {
systemKey := "system:" + fixtures.System.Hostname
systemKey := "system:" + snapshot.System.Hostname
systemDisk := float64(totalUsed) / float64(totalCap) * 100
mh.AddGuestMetric(systemKey, "disk", systemDisk, ts)
if ms != nil {
ms.Write("truenas", fixtures.System.Hostname, "disk", systemDisk, ts)
ms.Write("truenas", snapshot.System.Hostname, "disk", systemDisk, ts)
}
}
for _, pool := range fixtures.Pools {
for _, pool := range snapshot.Pools {
if pool.TotalBytes > 0 {
poolKey := "pool:" + pool.Name
diskPct := float64(pool.UsedBytes) / float64(pool.TotalBytes) * 100
@ -1034,7 +1035,7 @@ func recordTrueNASFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, ts time
}
}
for _, dataset := range fixtures.Datasets {
for _, dataset := range snapshot.Datasets {
totalBytes := dataset.UsedBytes + dataset.AvailBytes
if totalBytes > 0 {
dsKey := "dataset:" + dataset.Name
@ -1047,8 +1048,8 @@ func recordTrueNASFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, ts time
}
}
func recordVMwareFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, ts time.Time) {
snapshot := mock.GetPlatformFixtures().VMware
func recordVMwareFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, fixtures mock.PlatformFixtures, ts time.Time) {
snapshot := fixtures.VMware
datastoreUsage := vmwareDatastoreUsageByID(snapshot.Datastores)
for _, host := range snapshot.Hosts {
@ -1251,10 +1252,11 @@ func adaptContainers(cts []models.Container) []containerAdapter {
return result
}
func recordMockStateToMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.StateSnapshot, ts time.Time) {
func recordMockStateToMetricsHistory(mh *MetricsHistory, ms *metrics.Store, graph mock.FixtureGraph, ts time.Time) {
if mh == nil {
return
}
state := graph.State
for _, node := range state.Nodes {
if node.ID == "" || node.Status != "online" {
@ -1453,8 +1455,8 @@ func recordMockStateToMetricsHistory(mh *MetricsHistory, ms *metrics.Store, stat
}
// Record TrueNAS pool/dataset disk-usage live ticks
recordTrueNASFixturesMetrics(mh, ms, ts)
recordVMwareFixturesMetrics(mh, ms, ts)
recordTrueNASFixturesMetrics(mh, ms, graph.PlatformFixtures, ts)
recordVMwareFixturesMetrics(mh, ms, graph.PlatformFixtures, ts)
}
func diskMetricsResourceID(disk models.PhysicalDisk) string {
@ -1515,8 +1517,8 @@ func (m *Monitor) startMockMetricsSampler(ctx context.Context) {
Msg("Mock metrics sampler: seeding historical data")
// Keep mock trend generation in-memory only so production history in the
// persistent metrics store remains untouched while mock mode is active.
seedMockMetricsHistory(m.metricsHistory, nil, state, time.Now(), seedDuration, cfg.SampleInterval)
recordMockStateToMetricsHistory(m.metricsHistory, nil, state, time.Now())
seedMockMetricsHistory(m.metricsHistory, nil, graph, time.Now(), seedDuration, cfg.SampleInterval)
recordMockStateToMetricsHistory(m.metricsHistory, nil, graph, time.Now())
m.mockMetricsWg.Add(1)
go func() {
@ -1533,7 +1535,7 @@ func (m *Monitor) startMockMetricsSampler(ctx context.Context) {
if !mock.IsMockEnabled() {
continue
}
recordMockStateToMetricsHistory(m.metricsHistory, nil, mock.CurrentFixtureGraph().State, time.Now())
recordMockStateToMetricsHistory(m.metricsHistory, nil, mock.CurrentFixtureGraph(), time.Now())
}
}
}()

View file

@ -9,9 +9,15 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/models"
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
"github.com/rcourtman/pulse-go-rewrite/pkg/metrics"
)
func fixtureGraphWithState(state models.StateSnapshot) mock.FixtureGraph {
return mock.FixtureGraph{State: state}
}
func TestSeedMockMetricsHistory_PopulatesSeries(t *testing.T) {
now := time.Now()
@ -77,7 +83,7 @@ func TestSeedMockMetricsHistory_PopulatesSeries(t *testing.T) {
}
mh := NewMetricsHistory(1000, 24*time.Hour)
seedMockMetricsHistory(mh, nil, state, now, time.Hour, 30*time.Second)
seedMockMetricsHistory(mh, nil, fixtureGraphWithState(state), now, time.Hour, 30*time.Second)
nodeCPU := mh.GetNodeMetrics("node-1", "cpu", time.Hour)
if len(nodeCPU) < 10 {
@ -129,7 +135,7 @@ func TestSeedMockMetricsHistory_PopulatesKubernetesPodSeries(t *testing.T) {
}
mh := NewMetricsHistory(1000, 24*time.Hour)
seedMockMetricsHistory(mh, nil, state, now, time.Hour, 30*time.Second)
seedMockMetricsHistory(mh, nil, fixtureGraphWithState(state), now, time.Hour, 30*time.Second)
metricID := kubernetesPodMetricID(state.KubernetesClusters[0], state.KubernetesClusters[0].Pods[0])
if metricID == "" {
@ -203,7 +209,7 @@ func TestSeedMockMetricsHistory_SeedsMetricsStore(t *testing.T) {
defer store.Close()
mh := NewMetricsHistory(1000, 7*24*time.Hour)
seedMockMetricsHistory(mh, store, state, now, 7*24*time.Hour, time.Minute)
seedMockMetricsHistory(mh, store, fixtureGraphWithState(state), now, 7*24*time.Hour, time.Minute)
points, err := store.Query("vm", "vm-100", "cpu", now.Add(-7*24*time.Hour), now, 3600)
if err != nil {
@ -255,7 +261,7 @@ func TestSeedMockMetricsHistory_SeedsDiskTemperatureMetricsStore(t *testing.T) {
defer store.Close()
mh := NewMetricsHistory(1000, 7*24*time.Hour)
seedMockMetricsHistory(mh, store, state, now, 7*24*time.Hour, time.Minute)
seedMockMetricsHistory(mh, store, fixtureGraphWithState(state), now, 7*24*time.Hour, time.Minute)
points, err := store.Query("disk", "SERIAL-001", "smart_temp", now.Add(-7*24*time.Hour), now, 3600)
if err != nil {
@ -284,7 +290,12 @@ func TestSeedMockMetricsHistory_SeedsVMwareMetricsStore(t *testing.T) {
defer store.Close()
mh := NewMetricsHistory(1000, 7*24*time.Hour)
seedMockMetricsHistory(mh, store, state, now, 7*24*time.Hour, time.Minute)
seedMockMetricsHistory(mh, store, mock.FixtureGraph{
State: state,
PlatformFixtures: mock.PlatformFixtures{
VMware: vmware.DefaultFixtures(),
},
}, now, 7*24*time.Hour, time.Minute)
hostPoints, err := store.Query("agent", "vc-mock-1:host:host-101", "cpu", now.Add(-7*24*time.Hour), now, 3600)
if err != nil {
@ -314,7 +325,7 @@ func TestSeedMockMetricsHistory_SeedsVMwareMetricsStore(t *testing.T) {
func TestSeedMockMetricsHistory_SeedsTrueNASMetricsStore(t *testing.T) {
now := time.Now()
state := models.StateSnapshot{}
fixtures := mock.DefaultPlatformFixtures().TrueNAS
fixtures := truenas.DefaultFixtures()
if strings.TrimSpace(fixtures.System.Hostname) == "" {
t.Fatal("expected canonical truenas system hostname fixture")
@ -337,7 +348,12 @@ func TestSeedMockMetricsHistory_SeedsTrueNASMetricsStore(t *testing.T) {
defer store.Close()
mh := NewMetricsHistory(1000, 7*24*time.Hour)
seedMockMetricsHistory(mh, store, state, now, 7*24*time.Hour, time.Minute)
seedMockMetricsHistory(mh, store, mock.FixtureGraph{
State: state,
PlatformFixtures: mock.PlatformFixtures{
TrueNAS: fixtures,
},
}, now, 7*24*time.Hour, time.Minute)
systemPoints, err := store.Query("truenas", fixtures.System.Hostname, "disk", now.Add(-7*24*time.Hour), now, 3600)
if err != nil {
@ -408,7 +424,7 @@ func TestSeedMockMetricsHistory_UsesCanonicalMockFixtureGraphForLegacyAndProvide
defer store.Close()
mh := NewMetricsHistory(1000, 7*24*time.Hour)
seedMockMetricsHistory(mh, store, graph.State, now, 7*24*time.Hour, time.Minute)
seedMockMetricsHistory(mh, store, graph, now, 7*24*time.Hour, time.Minute)
nodePoints, err := store.Query("node", graph.State.Nodes[0].ID, "cpu", now.Add(-7*24*time.Hour), now, 3600)
if err != nil {

View file

@ -1194,7 +1194,7 @@ func (m *Monitor) GetConnectionStatuses() map[string]bool {
if mock.IsMockEnabled() {
statuses := make(map[string]bool)
state := mock.GetMockState()
state := mock.CurrentFixtureGraph().State
for _, node := range state.Nodes {
key := "pve-" + node.Name
statuses[key] = strings.ToLower(node.Status) == "online"
@ -2667,11 +2667,11 @@ func (m *Monitor) GetState() models.StateSnapshot {
// Check if mock mode is enabled
if mock.IsMockEnabled() {
state := mock.GetMockState()
state := mock.CurrentFixtureGraph().State
if state.ActiveAlerts == nil && m.alertManager != nil {
// Populate snapshot lazily if the cache hasn't been filled yet.
mock.UpdateAlertSnapshots(m.alertManager.GetActiveAlerts(), m.alertManager.GetRecentlyResolved())
state = mock.GetMockState()
state = mock.CurrentFixtureGraph().State
}
return state
}

View file

@ -223,7 +223,7 @@ func (m *Monitor) checkMockAlerts() {
}
// Get mock state
state := mock.GetMockState()
state := mock.CurrentFixtureGraph().State
log.Info().
Int("vms", len(state.VMs)).

View file

@ -20,7 +20,7 @@ func (m *Monitor) listRecoveryRollupsForAlerts(ctx context.Context, kind recover
}
if mock.IsMockEnabled() {
points := mock.GetMockRecoveryPoints()
points := mock.CurrentFixtureGraph().RecoveryPoints()
filtered := make([]recovery.RecoveryPoint, 0, len(points))
for _, p := range points {
if kind != "" && p.Kind != kind {