Modernize platform mock runtime fixtures

This commit is contained in:
rcourtman 2026-03-31 13:36:11 +01:00
parent c511638acc
commit a09f61d214
22 changed files with 651 additions and 118 deletions

View file

@ -266,6 +266,11 @@ running server between real and mock data, the canonical TrueNAS and VMware
settings routes must keep surfacing through the same Platform connections
workspace and handoff URLs instead of depending on process-start-only wiring
or a mock-only alternate shell.
That same lifecycle-owned mock path now also requires one shared fixture owner
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 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

@ -712,6 +712,12 @@ inventory through `source=truenas` and `source=vmware-vsphere`. Shared query
parsing may accept `vmware-vsphere` as the operator-facing VMware alias, but
the emitted canonical resource source remains the shared `vmware` source
family rather than a second backend source key.
That same runtime mock contract now also owns fixture authority. Mock
connection payloads returned from `/api/truenas/connections` and
`/api/vmware/connections` must derive from the shared `internal/mock/`
platform fixture owner rather than handler-local fixture assembly, so settings
payloads stay aligned with the unified runtime mock graph, storage/recovery
context, and seeded monitoring history.
That same VMware test contract now also owns structured setup-failure
classification. When `POST /api/vmware/connections/test` or
`POST /api/vmware/connections/{id}/test` fails, the backend payload must

View file

@ -365,6 +365,11 @@ When `/api/system/mock-mode` changes on a live server, the TrueNAS supplemental
provider must rebind immediately and repopulate the canonical read state so
settings, infrastructure, storage, and other shared consumers see the same
mock-backed inventory without restart.
That same runtime mock ownership now also includes fixture authority. Mock
TrueNAS and VMware inventory plus mock metrics-history seeding must derive from
one shared platform fixture owner in `internal/mock/` so settings payloads,
supplemental ingest, unified read-state, and seeded charts cannot drift from
each other when the v6 runtime runs in mock mode.
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

@ -148,6 +148,12 @@ querying, and the operator-facing storage health presentation layer.
data remains inventory-only context and must not be treated as proof of
restore capability, recovery artifacts, or widened platform recovery
support.
11. Keep runtime mock platform context derived from one shared fixture owner.
When shared `internal/api/` and monitoring wiring surface mock TrueNAS or
VMware storage/recovery-adjacent inventory, that data must come from the
canonical `internal/mock/` platform fixture layer so settings payloads,
unified inventory, and recovery/storage context stay aligned instead of
drifting through recovery-local fixture assembly.
## Current State
@ -1671,6 +1677,12 @@ runtime mock data. Mock TrueNAS pools/datasets and mock VMware datastores may
surface through the shared storage and recovery-adjacent pages as canonical
inventory context, but that visibility still does not widen Pulse's recovery
support claim or imply restore capability for either platform.
That same bounded mock contract now also requires one shared fixture authority.
When storage or recovery surfaces render mock TrueNAS pools, datasets,
snapshots, replication context, or mock VMware datastores, that inventory must
come from the same `internal/mock/` platform fixture owner that drives settings
payloads and unified runtime inventory, rather than recovery-local fixture
assembly or page-local compatibility fallbacks.
Storage and recovery must not infer VMware restore support, recovery rollups,
or VMware-local protection semantics from the presence of those datastores or
VM snapshot-read context on the shared pages.

View file

@ -348,6 +348,121 @@ func TestContract_PlatformMockToggleRebindsRuntimeConnectionsAndResources(t *tes
assertResourceSource("/api/resources?source=vmware-vsphere", unifiedresources.SourceVMware)
}
func TestContract_PlatformMockConnectionListsUseSharedFixtureMetadata(t *testing.T) {
setTrueNASFeatureForTest(t, true)
setVMwareFeatureForTest(t, true)
prevMock := mock.IsMockEnabled()
mock.SetEnabled(true)
t.Cleanup(func() {
mock.SetEnabled(prevMock)
})
t.Run("truenas", func(t *testing.T) {
fixture := mock.DefaultTrueNASConnectionFixture()
if fixture.CollectedAt.IsZero() {
t.Fatal("expected canonical truenas mock fixture timestamp")
}
handler, _, _ := newTrueNASHandlersForTest(t, nil)
req := httptest.NewRequest(http.MethodGet, "/api/truenas/connections", nil)
rec := httptest.NewRecorder()
handler.HandleList(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var responses []trueNASConnectionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil {
t.Fatalf("decode truenas mock list response: %v", err)
}
if len(responses) != 1 {
t.Fatalf("expected 1 mock truenas connection, got %d", len(responses))
}
response := responses[0]
if response.ID != fixture.ID || response.Name != fixture.Name || response.Host != fixture.Host || response.Port != fixture.Port {
t.Fatalf("unexpected truenas mock connection metadata: got %+v want fixture %+v", response.TrueNASInstance, fixture)
}
if response.APIKey != "********" {
t.Fatalf("expected redacted truenas api key, got %q", response.APIKey)
}
if response.Poll == nil || response.Poll.IntervalSeconds != fixture.PollIntervalSeconds {
t.Fatalf("expected truenas poll interval %d, got %+v", fixture.PollIntervalSeconds, response.Poll)
}
if response.Poll.LastSuccessAt == nil || !response.Poll.LastSuccessAt.Equal(fixture.CollectedAt) {
t.Fatalf("expected truenas last success at %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Poll)
}
if response.Observed == nil {
t.Fatal("expected truenas observed summary")
}
if response.Observed.Host != fixture.Host ||
response.Observed.ResourceID != fixture.ResourceID ||
response.Observed.Systems != fixture.Systems ||
response.Observed.StoragePools != fixture.StoragePools ||
response.Observed.Datasets != fixture.Datasets ||
response.Observed.Apps != fixture.Apps ||
response.Observed.Disks != fixture.Disks ||
response.Observed.RecoveryArtifacts != fixture.RecoveryArtifacts {
t.Fatalf("unexpected truenas observed summary: got %+v want fixture %+v", response.Observed, fixture)
}
if response.Observed.CollectedAt == nil || !response.Observed.CollectedAt.Equal(fixture.CollectedAt) {
t.Fatalf("expected truenas observed collectedAt %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Observed)
}
})
t.Run("vmware", func(t *testing.T) {
fixture := mock.DefaultVMwareConnectionFixture()
if fixture.CollectedAt.IsZero() {
t.Fatal("expected canonical vmware mock fixture timestamp")
}
handler, _ := newVMwareHandlersForTest(t)
req := httptest.NewRequest(http.MethodGet, "/api/vmware/connections", nil)
rec := httptest.NewRecorder()
handler.HandleList(rec, req)
if rec.Code != http.StatusOK {
t.Fatalf("expected 200, got %d: %s", rec.Code, rec.Body.String())
}
var responses []vmwareConnectionResponse
if err := json.Unmarshal(rec.Body.Bytes(), &responses); err != nil {
t.Fatalf("decode vmware mock list response: %v", err)
}
if len(responses) != 1 {
t.Fatalf("expected 1 mock vmware connection, got %d", len(responses))
}
response := responses[0]
if response.ID != fixture.ID || response.Name != fixture.Name || response.Host != fixture.Host || response.Port != fixture.Port || response.Username != fixture.Username {
t.Fatalf("unexpected vmware mock connection metadata: got %+v want fixture %+v", response.VMwareVCenterInstance, fixture)
}
if response.Password != "********" {
t.Fatalf("expected redacted vmware password, got %q", response.Password)
}
if response.Poll == nil || response.Poll.IntervalSeconds != fixture.PollIntervalSeconds {
t.Fatalf("expected vmware poll interval %d, got %+v", fixture.PollIntervalSeconds, response.Poll)
}
if response.Poll.LastSuccessAt == nil || !response.Poll.LastSuccessAt.Equal(fixture.CollectedAt) {
t.Fatalf("expected vmware last success at %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Poll)
}
if response.Observed == nil {
t.Fatal("expected vmware observed summary")
}
if response.Observed.Hosts != fixture.Hosts ||
response.Observed.VMs != fixture.VMs ||
response.Observed.Datastores != fixture.Datastores ||
response.Observed.VIRelease != fixture.VIRelease {
t.Fatalf("unexpected vmware observed summary: got %+v want fixture %+v", response.Observed, fixture)
}
if response.Observed.CollectedAt == nil || !response.Observed.CollectedAt.Equal(fixture.CollectedAt) {
t.Fatalf("expected vmware observed collectedAt %s, got %+v", fixture.CollectedAt.Format(time.RFC3339), response.Observed)
}
})
}
func TestContract_VMwareConnectionListCarriesDegradedObservedSummary(t *testing.T) {
setVMwareFeatureForTest(t, true)

View file

@ -1,87 +1,78 @@
package api
import (
"strings"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/monitoring"
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
)
const defaultMockPlatformPollIntervalSeconds = 60
func mockTrueNASConnectionResponses() []trueNASConnectionResponse {
fixtures := truenas.DefaultFixtures()
collectedAt := fixtures.CollectedAt
if collectedAt.IsZero() {
collectedAt = fixtures.System.CollectedAt
}
fixture := mock.DefaultTrueNASConnectionFixture()
instance := config.TrueNASInstance{
ID: "truenas-mock-1",
Name: "Archive NAS",
Host: strings.TrimSpace(fixtures.System.Hostname),
Port: 443,
APIKey: "mock-truenas-api-key",
UseHTTPS: true,
Enabled: true,
PollIntervalSecs: defaultMockPlatformPollIntervalSeconds,
ID: fixture.ID,
Name: fixture.Name,
Host: fixture.Host,
Port: fixture.Port,
APIKey: fixture.APIKey,
UseHTTPS: fixture.UseHTTPS,
Enabled: fixture.Enabled,
PollIntervalSecs: fixture.PollIntervalSeconds,
}
instance.ApplyDefaults()
observed := &monitoring.TrueNASConnectionObservedSummary{
Host: strings.TrimSpace(fixtures.System.Hostname),
ResourceID: strings.TrimSpace(fixtures.System.Hostname),
Systems: 1,
StoragePools: len(fixtures.Pools),
Datasets: len(fixtures.Datasets),
Apps: len(fixtures.Apps),
Disks: len(fixtures.Disks),
RecoveryArtifacts: len(fixtures.ZFSSnapshots) + len(fixtures.ReplicationTasks),
CollectedAt: mockPlatformTimePointer(collectedAt),
Host: fixture.Host,
ResourceID: fixture.ResourceID,
Systems: fixture.Systems,
StoragePools: fixture.StoragePools,
Datasets: fixture.Datasets,
Apps: fixture.Apps,
Disks: fixture.Disks,
RecoveryArtifacts: fixture.RecoveryArtifacts,
CollectedAt: mockPlatformTimePointer(fixture.CollectedAt),
}
return []trueNASConnectionResponse{{
TrueNASInstance: instance.Redacted(),
Poll: &monitoring.TrueNASConnectionPollStatus{
IntervalSeconds: instance.EffectivePollIntervalSecs(),
LastAttemptAt: mockPlatformTimePointer(collectedAt),
LastSuccessAt: mockPlatformTimePointer(collectedAt),
LastAttemptAt: mockPlatformTimePointer(fixture.CollectedAt),
LastSuccessAt: mockPlatformTimePointer(fixture.CollectedAt),
},
Observed: observed,
}}
}
func mockVMwareConnectionResponses() []vmwareConnectionResponse {
snapshot := vmware.DefaultFixtures()
collectedAt := snapshot.CollectedAt
fixture := mock.DefaultVMwareConnectionFixture()
instance := config.VMwareVCenterInstance{
ID: strings.TrimSpace(snapshot.ConnectionID),
Name: strings.TrimSpace(snapshot.ConnectionName),
Host: strings.TrimSpace(snapshot.VCenterHost),
Port: 443,
Username: "administrator@vsphere.local",
Password: "mock-vcenter-password",
Enabled: true,
ID: fixture.ID,
Name: fixture.Name,
Host: fixture.Host,
Port: fixture.Port,
Username: fixture.Username,
Password: fixture.Password,
Enabled: fixture.Enabled,
}
instance.ApplyDefaults()
return []vmwareConnectionResponse{{
VMwareVCenterInstance: instance.Redacted(),
Poll: &monitoring.VMwareConnectionPollStatus{
IntervalSeconds: defaultMockPlatformPollIntervalSeconds,
LastAttemptAt: mockPlatformTimePointer(collectedAt),
LastSuccessAt: mockPlatformTimePointer(collectedAt),
IntervalSeconds: fixture.PollIntervalSeconds,
LastAttemptAt: mockPlatformTimePointer(fixture.CollectedAt),
LastSuccessAt: mockPlatformTimePointer(fixture.CollectedAt),
},
Observed: &monitoring.VMwareConnectionObservedSummary{
CollectedAt: mockPlatformTimePointer(collectedAt),
Hosts: len(snapshot.Hosts),
VMs: len(snapshot.VMs),
Datastores: len(snapshot.Datastores),
VIRelease: strings.TrimSpace(snapshot.VIRelease),
CollectedAt: mockPlatformTimePointer(fixture.CollectedAt),
Hosts: fixture.Hosts,
VMs: fixture.VMs,
Datastores: fixture.Datastores,
VIRelease: fixture.VIRelease,
},
}}
}

View file

@ -3546,7 +3546,7 @@ func TestResourceListWithoutSupplementalProvider(t *testing.T) {
func TestSupplementalSnapshotOwnedSources_TrueNASProviders(t *testing.T) {
sources := supplementalSnapshotOwnedSources(map[unified.DataSource]SupplementalRecordsProvider{
unified.SourceTrueNAS: trueNASRecordsAdapter{},
unified.SourceTrueNAS: mockSupplementalRecordsAdapter{source: unified.SourceTrueNAS},
}, "default")
if len(sources) != 1 || sources[0] != unified.SourceTrueNAS {

View file

@ -1262,8 +1262,8 @@ func (r *Router) syncPlatformSupplementalProviders(mockEnabled bool) {
truenas.SetFeatureEnabled(true)
vmware.SetFeatureEnabled(true)
trueNASAdapter := trueNASRecordsAdapter{provider: truenas.NewDefaultProvider()}
vmwareAdapter := vmwareRecordsAdapter{provider: vmware.NewDefaultProvider()}
trueNASAdapter := mockSupplementalRecordsAdapter{source: unifiedresources.SourceTrueNAS}
vmwareAdapter := mockSupplementalRecordsAdapter{source: unifiedresources.SourceVMware}
if r.resourceHandlers != nil {
r.resourceHandlers.SetSupplementalRecordsProvider(unifiedresources.SourceTrueNAS, trueNASAdapter)
@ -1274,6 +1274,9 @@ func (r *Router) syncPlatformSupplementalProviders(mockEnabled bool) {
return
}
truenas.ResetFeatureEnabledFromEnv()
vmware.ResetFeatureEnabledFromEnv()
if r.resourceHandlers != nil {
r.resourceHandlers.SetSupplementalRecordsProvider(unifiedresources.SourceTrueNAS, r.trueNASPoller)
r.resourceHandlers.SetSupplementalRecordsProvider(unifiedresources.SourceVMware, r.vmwarePoller)
@ -9063,66 +9066,35 @@ func (w *knowledgeStoreProviderWrapper) GetKnowledge(resourceID string, category
return result
}
// trueNASRecordsAdapter wraps a truenas.Provider to satisfy SupplementalRecordsProvider.
type trueNASRecordsAdapter struct {
provider *truenas.Provider
type mockSupplementalRecordsAdapter struct {
source unifiedresources.DataSource
}
func (a trueNASRecordsAdapter) GetCurrentRecords() []unifiedresources.IngestRecord {
// Legacy behavior: treat this mock adapter as default-org scoped.
func (a mockSupplementalRecordsAdapter) GetCurrentRecords() []unifiedresources.IngestRecord {
return a.GetCurrentRecordsForOrg("default")
}
func (a trueNASRecordsAdapter) GetCurrentRecordsForOrg(orgID string) []unifiedresources.IngestRecord {
func (a mockSupplementalRecordsAdapter) GetCurrentRecordsForOrg(orgID string) []unifiedresources.IngestRecord {
if strings.TrimSpace(orgID) != "" && strings.TrimSpace(orgID) != "default" {
return nil
}
if a.provider == nil {
return nil
}
return a.provider.Records()
return mock.SupplementalRecords(a.source)
}
func (a trueNASRecordsAdapter) SupplementalRecords(_ *monitoring.Monitor, orgID string) []unifiedresources.IngestRecord {
func (a mockSupplementalRecordsAdapter) SupplementalRecords(_ *monitoring.Monitor, orgID string) []unifiedresources.IngestRecord {
return a.GetCurrentRecordsForOrg(orgID)
}
func (a trueNASRecordsAdapter) SnapshotOwnedSources() []unifiedresources.DataSource {
return []unifiedresources.DataSource{unifiedresources.SourceTrueNAS}
}
func (a trueNASRecordsAdapter) SnapshotOwnedSourcesForOrg(string) []unifiedresources.DataSource {
return []unifiedresources.DataSource{unifiedresources.SourceTrueNAS}
}
type vmwareRecordsAdapter struct {
provider *vmware.Provider
}
func (a vmwareRecordsAdapter) GetCurrentRecords() []unifiedresources.IngestRecord {
return a.GetCurrentRecordsForOrg("default")
}
func (a vmwareRecordsAdapter) GetCurrentRecordsForOrg(orgID string) []unifiedresources.IngestRecord {
if strings.TrimSpace(orgID) != "" && strings.TrimSpace(orgID) != "default" {
func (a mockSupplementalRecordsAdapter) SnapshotOwnedSources() []unifiedresources.DataSource {
normalized := normalizeDataSourceAlias(a.source)
if normalized == "" {
return nil
}
if a.provider == nil {
return nil
}
return a.provider.Records()
return []unifiedresources.DataSource{normalized}
}
func (a vmwareRecordsAdapter) SupplementalRecords(_ *monitoring.Monitor, orgID string) []unifiedresources.IngestRecord {
return a.GetCurrentRecordsForOrg(orgID)
}
func (a vmwareRecordsAdapter) SnapshotOwnedSources() []unifiedresources.DataSource {
return []unifiedresources.DataSource{unifiedresources.SourceVMware}
}
func (a vmwareRecordsAdapter) SnapshotOwnedSourcesForOrg(string) []unifiedresources.DataSource {
return []unifiedresources.DataSource{unifiedresources.SourceVMware}
func (a mockSupplementalRecordsAdapter) SnapshotOwnedSourcesForOrg(string) []unifiedresources.DataSource {
return a.SnapshotOwnedSources()
}
// trigger rebuild Fri Jan 16 10:52:41 UTC 2026

View file

@ -8,7 +8,9 @@ import (
"github.com/rcourtman/pulse-go-rewrite/internal/config"
"github.com/rcourtman/pulse-go-rewrite/internal/mock"
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
)
func TestRouterMockMode_SeedsTrueNASAndVMwareSupplementalResources(t *testing.T) {
@ -67,3 +69,39 @@ func TestRouterMockMode_SeedsTrueNASAndVMwareSupplementalResources(t *testing.T)
})
}
}
func TestRouterMockMode_RestoresPlatformFeatureFlagsAfterDisable(t *testing.T) {
t.Setenv(truenas.FeatureTrueNAS, "false")
t.Setenv(vmware.FeatureVMware, "false")
previousTrueNAS := truenas.IsFeatureEnabled()
previousVMware := vmware.IsFeatureEnabled()
truenas.ResetFeatureEnabledFromEnv()
vmware.ResetFeatureEnabledFromEnv()
t.Cleanup(func() {
truenas.SetFeatureEnabled(previousTrueNAS)
vmware.SetFeatureEnabled(previousVMware)
})
cfg := &config.Config{DataPath: t.TempDir()}
router := NewRouter(cfg, nil, nil, nil, nil, "1.0.0")
t.Cleanup(func() {
router.shutdownBackgroundWorkers()
})
router.syncPlatformSupplementalProviders(true)
if !truenas.IsFeatureEnabled() {
t.Fatal("expected mock mode to force-enable TrueNAS feature flag")
}
if !vmware.IsFeatureEnabled() {
t.Fatal("expected mock mode to force-enable VMware feature flag")
}
router.syncPlatformSupplementalProviders(false)
if truenas.IsFeatureEnabled() {
t.Fatal("expected disabling mock mode to restore TrueNAS feature flag from env")
}
if vmware.IsFeatureEnabled() {
t.Fatal("expected disabling mock mode to restore VMware feature flag from env")
}
}

View file

@ -0,0 +1,174 @@
package mock
import (
"strings"
"time"
"github.com/rcourtman/pulse-go-rewrite/internal/truenas"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
"github.com/rcourtman/pulse-go-rewrite/internal/vmware"
)
const DefaultPlatformPollIntervalSeconds = 60
type PlatformFixtures struct {
TrueNAS truenas.FixtureSnapshot
VMware vmware.InventorySnapshot
}
type TrueNASConnectionFixture struct {
ID string
Name string
Host string
Port int
APIKey string
UseHTTPS bool
Enabled bool
PollIntervalSeconds int
CollectedAt time.Time
ResourceID string
Systems int
StoragePools int
Datasets int
Apps int
Disks int
RecoveryArtifacts int
}
type VMwareConnectionFixture struct {
ID string
Name string
Host string
Port int
Username string
Password string
Enabled bool
PollIntervalSeconds int
CollectedAt time.Time
Hosts int
VMs int
Datastores int
VIRelease string
}
func DefaultPlatformFixtures() PlatformFixtures {
return PlatformFixtures{
TrueNAS: truenas.DefaultFixtures(),
VMware: vmware.DefaultFixtures(),
}
}
func DefaultTrueNASConnectionFixture() TrueNASConnectionFixture {
fixtures := DefaultPlatformFixtures().TrueNAS
collectedAt := trueNASCollectedAt(fixtures)
host := strings.TrimSpace(fixtures.System.Hostname)
return TrueNASConnectionFixture{
ID: "truenas-mock-1",
Name: "Archive NAS",
Host: host,
Port: 443,
APIKey: "mock-truenas-api-key",
UseHTTPS: true,
Enabled: true,
PollIntervalSeconds: DefaultPlatformPollIntervalSeconds,
CollectedAt: collectedAt,
ResourceID: host,
Systems: 1,
StoragePools: len(fixtures.Pools),
Datasets: len(fixtures.Datasets),
Apps: len(fixtures.Apps),
Disks: len(fixtures.Disks),
RecoveryArtifacts: len(fixtures.ZFSSnapshots) + len(fixtures.ReplicationTasks),
}
}
func DefaultVMwareConnectionFixture() VMwareConnectionFixture {
fixtures := DefaultPlatformFixtures().VMware
return VMwareConnectionFixture{
ID: strings.TrimSpace(fixtures.ConnectionID),
Name: strings.TrimSpace(fixtures.ConnectionName),
Host: strings.TrimSpace(fixtures.VCenterHost),
Port: 443,
Username: "administrator@vsphere.local",
Password: "mock-vcenter-password",
Enabled: true,
PollIntervalSeconds: DefaultPlatformPollIntervalSeconds,
CollectedAt: fixtures.CollectedAt,
Hosts: len(fixtures.Hosts),
VMs: len(fixtures.VMs),
Datastores: len(fixtures.Datastores),
VIRelease: strings.TrimSpace(fixtures.VIRelease),
}
}
func SupplementalRecords(source unifiedresources.DataSource) []unifiedresources.IngestRecord {
switch normalizePlatformSource(source) {
case unifiedresources.SourceTrueNAS:
return truenas.FixtureRecords(DefaultPlatformFixtures().TrueNAS)
case unifiedresources.SourceVMware:
return vmware.FixtureRecords(DefaultPlatformFixtures().VMware)
default:
return nil
}
}
func PlatformOwnedSources() []unifiedresources.DataSource {
return []unifiedresources.DataSource{
unifiedresources.SourceTrueNAS,
unifiedresources.SourceVMware,
}
}
func UnifiedResourceSnapshot() ([]unifiedresources.Resource, time.Time) {
if !IsMockEnabled() {
return nil, time.Time{}
}
fixtures := DefaultPlatformFixtures()
snapshot := GetMockState()
registry := unifiedresources.NewRegistry(nil)
registry.IngestSnapshot(unifiedresources.SnapshotWithoutSources(snapshot, PlatformOwnedSources()))
for _, source := range PlatformOwnedSources() {
records := SupplementalRecords(source)
if len(records) == 0 {
continue
}
registry.IngestRecords(source, records)
}
freshness := snapshot.LastUpdate
for _, candidate := range []time.Time{
trueNASCollectedAt(fixtures.TrueNAS),
fixtures.VMware.CollectedAt,
} {
if candidate.IsZero() {
continue
}
if freshness.IsZero() || candidate.After(freshness) {
freshness = candidate
}
}
return registry.List(), freshness
}
func trueNASCollectedAt(fixtures truenas.FixtureSnapshot) time.Time {
if !fixtures.CollectedAt.IsZero() {
return fixtures.CollectedAt
}
return fixtures.System.CollectedAt
}
func normalizePlatformSource(source unifiedresources.DataSource) unifiedresources.DataSource {
switch strings.ToLower(strings.TrimSpace(string(source))) {
case "truenas":
return unifiedresources.SourceTrueNAS
case "vmware", "vmware-vsphere":
return unifiedresources.SourceVMware
default:
return ""
}
}

View file

@ -0,0 +1,44 @@
package mock
import (
"testing"
"github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
)
func TestUnifiedResourceSnapshotIncludesPlatformFixtures(t *testing.T) {
previous := IsMockEnabled()
SetEnabled(true)
t.Cleanup(func() { SetEnabled(previous) })
resources, freshness := UnifiedResourceSnapshot()
if len(resources) == 0 {
t.Fatal("expected unified resources in mock mode")
}
if freshness.IsZero() {
t.Fatal("expected non-zero freshness for mock unified resources")
}
wantNames := map[string]bool{
"truenas-main": false,
"esxi-01.lab.local": false,
"orders-api-01": false,
}
for _, resource := range resources {
if _, ok := wantNames[resource.Name]; ok {
wantNames[resource.Name] = true
}
}
for name, found := range wantNames {
if !found {
t.Fatalf("expected mock unified resources to include %q", name)
}
}
}
func TestSupplementalRecordsNormalizesVMwareAlias(t *testing.T) {
records := SupplementalRecords(unifiedresources.DataSource("vmware-vsphere"))
if len(records) == 0 {
t.Fatal("expected records for vmware-vsphere alias")
}
}

View file

@ -376,12 +376,22 @@ func generateMockRecoveryPoints() []recovery.RecoveryPoint {
}
// TrueNAS: 3 dataset subjects with multiple points over time.
truenasConnID := "truenas-mock-1"
truenasHost := "truenas.local"
truenasDatasets := []string{
"tank/apps/postgres",
"tank/apps/minio",
"tank/media/photos",
truenasConnection := DefaultTrueNASConnectionFixture()
truenasConnID := truenasConnection.ID
truenasHost := truenasConnection.Host
truenasDatasets := make([]string, 0, 3)
for _, dataset := range DefaultPlatformFixtures().TrueNAS.Datasets {
name := strings.TrimSpace(dataset.Name)
if name == "" {
continue
}
truenasDatasets = append(truenasDatasets, name)
if len(truenasDatasets) == 3 {
break
}
}
if len(truenasDatasets) == 0 {
truenasDatasets = []string{"tank/apps", "tank/media", "archive/backups"}
}
// ZFS snapshots (snapshot / snapshot): success points with meaningful details.

View file

@ -260,6 +260,23 @@ func TestUnifiedAppContainerMetricsUseCanonicalGuestHistoryPath(t *testing.T) {
}
}
func TestMockUnifiedStateViewUsesCanonicalMockFixtureGraph(t *testing.T) {
data, err := os.ReadFile("monitor.go")
if err != nil {
t.Fatalf("failed to read monitor.go: %v", err)
}
source := string(data)
requiredSnippets := []string{
"resources, freshness := mock.UnifiedResourceSnapshot()",
"return monitorUnifiedStateViewFromResources(resources, freshness)",
}
for _, snippet := range requiredSnippets {
if !strings.Contains(source, snippet) {
t.Fatalf("monitor.go must contain %q", snippet)
}
}
}
func TestUnifiedAgentMetricsUseCanonicalHostHistoryPath(t *testing.T) {
data, err := os.ReadFile("monitor.go")
if err != nil {

View file

@ -11,7 +11,6 @@ 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"
"github.com/rs/zerolog/log"
@ -876,13 +875,13 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.
time.Sleep(50 * time.Millisecond)
}
// Seed TrueNAS pool/dataset disk-usage metrics
fixtures := truenas.DefaultFixtures()
log.Debug().Int("pools", len(fixtures.Pools)).Int("datasets", len(fixtures.Datasets)).Msg("mock seeding: processing TrueNAS fixtures")
platformFixtures := mock.DefaultPlatformFixtures()
trueNASFixtures := platformFixtures.TrueNAS
log.Debug().Int("pools", len(trueNASFixtures.Pools)).Int("datasets", len(trueNASFixtures.Datasets)).Msg("mock seeding: processing TrueNAS fixtures")
// System host: aggregated disk usage across all pools
totalCap, totalUsed := int64(0), int64(0)
for _, pool := range fixtures.Pools {
for _, pool := range trueNASFixtures.Pools {
totalCap += pool.TotalBytes
totalUsed += pool.UsedBytes
}
@ -890,10 +889,10 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.
if totalCap > 0 {
systemDisk = float64(totalUsed) / float64(totalCap) * 100
}
systemKey := "system:" + fixtures.System.Hostname
recordGuest(systemKey, "truenas", fixtures.System.Hostname, 0, 0, systemDisk, 0, 0, 0, 0, true, false, false)
systemKey := "system:" + trueNASFixtures.System.Hostname
recordGuest(systemKey, "truenas", trueNASFixtures.System.Hostname, 0, 0, systemDisk, 0, 0, 0, 0, true, false, false)
for _, pool := range fixtures.Pools {
for _, pool := range trueNASFixtures.Pools {
poolKey := "pool:" + pool.Name
diskPercent := float64(0)
if pool.TotalBytes > 0 {
@ -910,7 +909,7 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.
queueMetric("pool", pool.Name, "disk", diskPercent, now)
}
for _, dataset := range fixtures.Datasets {
for _, dataset := range trueNASFixtures.Datasets {
dsKey := "dataset:" + dataset.Name
totalBytes := dataset.UsedBytes + dataset.AvailBytes
diskPercent := float64(0)
@ -928,7 +927,7 @@ func seedMockMetricsHistory(mh *MetricsHistory, ms *metrics.Store, state models.
queueMetric("dataset", dataset.Name, "disk", diskPercent, now)
}
vmwareFixtures := vmware.DefaultFixtures()
vmwareFixtures := platformFixtures.VMware
vmwareDatastoreUsage := vmwareDatastoreUsageByID(vmwareFixtures.Datastores)
log.Debug().
Int("hosts", len(vmwareFixtures.Hosts)).
@ -1008,7 +1007,7 @@ 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 := truenas.DefaultFixtures()
fixtures := mock.DefaultPlatformFixtures().TrueNAS
totalCap, totalUsed := int64(0), int64(0)
for _, pool := range fixtures.Pools {
@ -1049,7 +1048,7 @@ func recordTrueNASFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, ts time
}
func recordVMwareFixturesMetrics(mh *MetricsHistory, ms *metrics.Store, ts time.Time) {
snapshot := vmware.DefaultFixtures()
snapshot := mock.DefaultPlatformFixtures().VMware
datastoreUsage := vmwareDatastoreUsageByID(snapshot.Datastores)
for _, host := range snapshot.Hosts {

View file

@ -3,6 +3,7 @@ package monitoring
import (
"context"
"math"
"strings"
"testing"
"time"
@ -310,6 +311,51 @@ func TestSeedMockMetricsHistory_SeedsVMwareMetricsStore(t *testing.T) {
}
}
func TestSeedMockMetricsHistory_SeedsTrueNASMetricsStore(t *testing.T) {
now := time.Now()
state := models.StateSnapshot{}
fixtures := mock.DefaultPlatformFixtures().TrueNAS
if strings.TrimSpace(fixtures.System.Hostname) == "" {
t.Fatal("expected canonical truenas system hostname fixture")
}
if len(fixtures.Datasets) == 0 {
t.Fatal("expected canonical truenas dataset fixtures")
}
cfg := metrics.DefaultConfig(t.TempDir())
cfg.RetentionRaw = 90 * 24 * time.Hour
cfg.RetentionMinute = 90 * 24 * time.Hour
cfg.RetentionHourly = 90 * 24 * time.Hour
cfg.RetentionDaily = 90 * 24 * time.Hour
cfg.WriteBufferSize = 500
store, err := metrics.NewStore(cfg)
if err != nil {
t.Fatalf("failed to create metrics store: %v", err)
}
defer store.Close()
mh := NewMetricsHistory(1000, 7*24*time.Hour)
seedMockMetricsHistory(mh, store, state, 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 {
t.Fatalf("failed to query TrueNAS system disk metrics: %v", err)
}
if len(systemPoints) == 0 {
t.Fatal("expected metrics store to have seeded TrueNAS system disk points")
}
datasetPoints, err := store.Query("dataset", fixtures.Datasets[0].Name, "disk", now.Add(-7*24*time.Hour), now, 3600)
if err != nil {
t.Fatalf("failed to query TrueNAS dataset disk metrics: %v", err)
}
if len(datasetPoints) == 0 {
t.Fatal("expected metrics store to have seeded TrueNAS dataset disk points")
}
}
func TestStartMockMetricsSampler_DoesNotClearExistingMetricsStoreData(t *testing.T) {
t.Setenv("PULSE_MOCK_NODES", "1")
t.Setenv("PULSE_MOCK_VMS_PER_NODE", "0")

View file

@ -4004,6 +4004,10 @@ func (m *Monitor) currentUnifiedStateView() monitorUnifiedStateView {
}
if mock.IsMockEnabled() {
resources, freshness := mock.UnifiedResourceSnapshot()
if len(resources) > 0 || !freshness.IsZero() {
return monitorUnifiedStateViewFromResources(resources, freshness)
}
return monitorUnifiedStateViewFromSnapshot(m.GetState())
}
@ -4058,8 +4062,8 @@ func unifiedResourceFreshness(store ResourceStoreInterface, state *models.State)
}
// UnifiedResourceSnapshot returns a canonical unified-resource seed plus the
// associated freshness marker. It respects mock mode by falling back to the
// current mock-aware state snapshot instead of the live resource store.
// associated freshness marker. In mock mode it returns the shared mock
// unified-resource fixture graph rather than the live resource store.
func (m *Monitor) UnifiedResourceSnapshot() ([]unifiedresources.Resource, time.Time) {
view := m.currentUnifiedStateView()
return view.resources, view.freshness

View file

@ -177,7 +177,7 @@ func TestMonitorUnifiedResourceSnapshotPrefersStoreFreshness(t *testing.T) {
}
}
func TestMonitorGetUnifiedReadStateOrSnapshotUsesMockSnapshotInsteadOfStore(t *testing.T) {
func TestMonitorGetUnifiedReadStateOrSnapshotUsesCanonicalMockUnifiedResources(t *testing.T) {
mock.SetEnabled(true)
t.Cleanup(func() { mock.SetEnabled(false) })
@ -207,7 +207,14 @@ func TestMonitorGetUnifiedReadStateOrSnapshotUsesMockSnapshotInsteadOfStore(t *t
t.Fatalf("expected read-state adapter with GetAll, got %T", readState)
}
if hasUnifiedResource(getter.GetAll(), "store-only-resource") {
resources := getter.GetAll()
if hasUnifiedResource(resources, "store-only-resource") {
t.Fatal("expected mock-mode read-state to ignore live resource store data")
}
if !hasUnifiedResourceName(resources, "truenas-main") {
t.Fatalf("expected mock-mode read-state to include TrueNAS mock resources, got %#v", resources)
}
if !hasUnifiedResourceName(resources, "esxi-01.lab.local") {
t.Fatalf("expected mock-mode read-state to include VMware mock resources, got %#v", resources)
}
}

View file

@ -0,0 +1,23 @@
package truenas
import "testing"
func TestFixtureRecordsIgnoreFeatureFlag(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(false)
t.Cleanup(func() { SetFeatureEnabled(previous) })
records := FixtureRecords(DefaultFixtures())
if len(records) == 0 {
t.Fatal("expected fixture records even when feature flag is disabled")
}
systemID := systemSourceID("truenas-main")
for _, record := range records {
if record.SourceID == systemID {
return
}
}
t.Fatalf("expected fixture records to include system source %q", systemID)
}

View file

@ -40,6 +40,12 @@ func SetFeatureEnabled(enabled bool) {
featureTrueNASEnabled.Store(enabled)
}
// ResetFeatureEnabledFromEnv restores the feature flag from the current
// environment configuration.
func ResetFeatureEnabledFromEnv() {
featureTrueNASEnabled.Store(parseFeatureEnabled(os.Getenv(FeatureTrueNAS)))
}
// Fetcher loads a TrueNAS snapshot from a concrete source.
type Fetcher interface {
Fetch(ctx context.Context) (*FixtureSnapshot, error)
@ -415,20 +421,36 @@ func (p *Provider) Snapshot() *FixtureSnapshot {
return snapshot
}
// FixtureRecords projects a TrueNAS fixture snapshot into canonical unified
// resource ingest records without consulting the runtime feature flag.
func FixtureRecords(snapshot FixtureSnapshot) []unifiedresources.IngestRecord {
return truenasRecordsFromSnapshot(&snapshot, nil)
}
// Records returns unified records if the feature flag is enabled.
func (p *Provider) Records() []unifiedresources.IngestRecord {
if p == nil || !IsFeatureEnabled() {
return nil
}
snapshot := p.Snapshot()
return truenasRecordsFromSnapshot(p.Snapshot(), p.now)
}
func truenasRecordsFromSnapshot(snapshot *FixtureSnapshot, now func() time.Time) []unifiedresources.IngestRecord {
if snapshot == nil {
return nil
}
collectedAt := snapshot.CollectedAt
if collectedAt.IsZero() {
collectedAt = p.now()
collectedAt = snapshot.System.CollectedAt
}
if collectedAt.IsZero() {
if now != nil {
collectedAt = now().UTC()
} else {
collectedAt = time.Now().UTC()
}
}
systemSourceID := systemSourceID(snapshot.System.Hostname)
systemAssessment := assessSystemStorage(snapshot)

View file

@ -45,6 +45,12 @@ func SetFeatureEnabled(enabled bool) {
featureVMwareEnabled.Store(enabled)
}
// ResetFeatureEnabledFromEnv restores the feature flag from the current
// environment configuration.
func ResetFeatureEnabledFromEnv() {
featureVMwareEnabled.Store(parseFeatureEnabled(os.Getenv(FeatureVMware)))
}
func parseFeatureEnabled(raw string) bool {
switch strings.TrimSpace(strings.ToLower(raw)) {
case "", "1", "true", "yes", "on":

View file

@ -0,0 +1,24 @@
package vmware
import "testing"
func TestFixtureRecordsIgnoreFeatureFlag(t *testing.T) {
previous := IsFeatureEnabled()
SetFeatureEnabled(false)
t.Cleanup(func() { SetFeatureEnabled(previous) })
snapshot := DefaultFixtures()
records := FixtureRecords(snapshot)
if len(records) == 0 {
t.Fatal("expected fixture records even when feature flag is disabled")
}
hostID := vmwareSourceID(snapshot.ConnectionID, "host", snapshot.Hosts[0].Host)
for _, record := range records {
if record.SourceID == hostID {
return
}
}
t.Fatalf("expected fixture records to include host source %q", hostID)
}

View file

@ -311,20 +311,33 @@ func (p *Provider) Snapshot() *InventorySnapshot {
return snapshot
}
// FixtureRecords projects a VMware fixture snapshot into canonical unified
// resource ingest records without consulting the runtime feature flag.
func FixtureRecords(snapshot InventorySnapshot) []unifiedresources.IngestRecord {
return vmwareRecordsFromSnapshot(&snapshot, nil)
}
// Records returns canonical VMware unified resources if the integration is enabled.
func (p *Provider) Records() []unifiedresources.IngestRecord {
if p == nil || !IsFeatureEnabled() {
return nil
}
snapshot := p.Snapshot()
return vmwareRecordsFromSnapshot(p.Snapshot(), p.now)
}
func vmwareRecordsFromSnapshot(snapshot *InventorySnapshot, now func() time.Time) []unifiedresources.IngestRecord {
if snapshot == nil {
return nil
}
collectedAt := snapshot.CollectedAt
if collectedAt.IsZero() {
collectedAt = p.now()
if now != nil {
collectedAt = now().UTC()
} else {
collectedAt = time.Now().UTC()
}
}
connectionName := firstNonEmptyTrimmed(snapshot.ConnectionName, snapshot.VCenterHost, snapshot.ConnectionID)