diff --git a/internal/agentcapabilities/purehelpers_branchcov0719late_test.go b/internal/agentcapabilities/purehelpers_branchcov0719late_test.go new file mode 100644 index 000000000..8a22599ae --- /dev/null +++ b/internal/agentcapabilities/purehelpers_branchcov0719late_test.go @@ -0,0 +1,265 @@ +package agentcapabilities + +import ( + "testing" +) + +// TestFirstStringPayloadValueAgentCapsBranchcov0719late covers the unexported +// firstStringPayloadValue helper across all of its branches: key present and a +// non-empty string (with surrounding whitespace that must be trimmed), key +// present but a non-string type, key present but an empty/whitespace-only +// string, key absent entirely, multiple keys where a later key is the first +// usable match, and the no-match empty return (including the degenerate +// no-keys case). +func TestFirstStringPayloadValueAgentCapsBranchcov0719late(t *testing.T) { + cases := []struct { + name string + payload map[string]any + keys []string + want string + }{ + { + name: "key present as non-empty string returns trimmed value", + payload: map[string]any{"command": " reboot host "}, + keys: []string{"command"}, + want: "reboot host", + }, + { + name: "key present as non-empty string with no surrounding whitespace passes through", + payload: map[string]any{"command": "reboot host"}, + keys: []string{"command"}, + want: "reboot host", + }, + { + name: "key present but wrong type int is skipped", + payload: map[string]any{"command": 42}, + keys: []string{"command"}, + want: "", + }, + { + name: "key present but wrong type bool is skipped", + payload: map[string]any{"command": true}, + keys: []string{"command"}, + want: "", + }, + { + name: "key present but wrong type slice is skipped", + payload: map[string]any{"command": []string{"a", "b"}}, + keys: []string{"command"}, + want: "", + }, + { + name: "key present but empty string is skipped", + payload: map[string]any{"command": ""}, + keys: []string{"command"}, + want: "", + }, + { + name: "key present but whitespace-only string is skipped", + payload: map[string]any{"command": " "}, + keys: []string{"command"}, + want: "", + }, + { + name: "key absent entirely returns empty", + payload: map[string]any{"unrelated": "value"}, + keys: []string{"command"}, + want: "", + }, + { + name: "nil payload returns empty", + payload: nil, + keys: []string{"command"}, + want: "", + }, + { + name: "multiple keys where first key matches returns first", + payload: map[string]any{"command": "first", "reason": "second"}, + keys: []string{"command", "reason"}, + want: "first", + }, + { + name: "multiple keys where later key matches after wrong type returns later", + payload: map[string]any{"command": 42, "reason": "real reason"}, + keys: []string{"command", "reason"}, + want: "real reason", + }, + { + name: "multiple keys where later key matches after whitespace-only returns later", + payload: map[string]any{"command": " ", "reason": "real reason"}, + keys: []string{"command", "reason"}, + want: "real reason", + }, + { + name: "multiple keys where later key matches after absent first returns later", + payload: map[string]any{"reason": "real reason"}, + keys: []string{"command", "reason"}, + want: "real reason", + }, + { + name: "multiple keys all wrong type returns empty", + payload: map[string]any{"command": 1, "reason": true}, + keys: []string{"command", "reason"}, + want: "", + }, + { + name: "multiple keys all whitespace-only returns empty", + payload: map[string]any{"command": " ", "reason": "\t\n"}, + keys: []string{"command", "reason"}, + want: "", + }, + { + name: "no keys provided returns empty", + payload: map[string]any{"command": "value"}, + keys: nil, + want: "", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := firstStringPayloadValue(tc.payload, tc.keys...) + if got != tc.want { + t.Fatalf("firstStringPayloadValue(%v, %v) = %q, want %q", tc.payload, tc.keys, got, tc.want) + } + }) + } +} + +// TestMCPManifestPromptProjectionSupportedAgentCapsBranchcov0719late covers both arms +// of MCPManifestPromptProjectionSupported and its unexported helper +// mcpManifestPromptProjectionSupported: a manifest whose Pulse workflow prompts +// include at least one prompt with a non-empty trimmed Name returns true, while +// a manifest with no workflow prompts or only blank-named prompts returns false. +func TestMCPManifestPromptProjectionSupportedAgentCapsBranchcov0719late(t *testing.T) { + t.Run("exported returns true when a workflow prompt has a non-empty name", func(t *testing.T) { + manifest := Manifest{ + WorkflowPrompts: []PulseWorkflowPrompt{ + {Name: "triage_fleet", Description: "Triage the fleet"}, + }, + } + if !MCPManifestPromptProjectionSupported(manifest) { + t.Fatalf("MCPManifestPromptProjectionSupported = false, want true for manifest with a named workflow prompt") + } + }) + + t.Run("exported returns true when only one of several prompts has a non-empty name", func(t *testing.T) { + manifest := Manifest{ + WorkflowPrompts: []PulseWorkflowPrompt{ + {Name: " "}, + {Name: ""}, + {Name: "review_finding"}, + }, + } + if !MCPManifestPromptProjectionSupported(manifest) { + t.Fatalf("MCPManifestPromptProjectionSupported = false, want true when any prompt has a non-blank name") + } + }) + + t.Run("exported returns false when no workflow prompts are declared", func(t *testing.T) { + manifest := Manifest{} + if MCPManifestPromptProjectionSupported(manifest) { + t.Fatalf("MCPManifestPromptProjectionSupported = true, want false for manifest without workflow prompts") + } + }) + + t.Run("exported returns false when workflow prompts only have blank names", func(t *testing.T) { + manifest := Manifest{ + WorkflowPrompts: []PulseWorkflowPrompt{ + {Name: " "}, + {Name: ""}, + }, + } + if MCPManifestPromptProjectionSupported(manifest) { + t.Fatalf("MCPManifestPromptProjectionSupported = true, want false when only blank-named prompts exist") + } + }) + + t.Run("exported returns true for a name that needs trimming", func(t *testing.T) { + manifest := Manifest{ + WorkflowPrompts: []PulseWorkflowPrompt{ + {Name: " investigate_resource "}, + }, + } + if !MCPManifestPromptProjectionSupported(manifest) { + t.Fatalf("MCPManifestPromptProjectionSupported = false, want true for a name that becomes non-empty after trimming") + } + }) + + t.Run("unexported helper returns true when a workflow prompt has a non-empty name", func(t *testing.T) { + manifest := Manifest{ + WorkflowPrompts: []PulseWorkflowPrompt{ + {Name: "operations_loop"}, + }, + } + if !mcpManifestPromptProjectionSupported(manifest) { + t.Fatalf("mcpManifestPromptProjectionSupported = false, want true") + } + }) + + t.Run("unexported helper returns false when no prompts are declared", func(t *testing.T) { + manifest := Manifest{} + if mcpManifestPromptProjectionSupported(manifest) { + t.Fatalf("mcpManifestPromptProjectionSupported = true, want false for empty manifest") + } + }) + + t.Run("unexported helper returns false when only blank-named prompts exist", func(t *testing.T) { + manifest := Manifest{ + WorkflowPrompts: []PulseWorkflowPrompt{ + {Name: " "}, + }, + } + if mcpManifestPromptProjectionSupported(manifest) { + t.Fatalf("mcpManifestPromptProjectionSupported = true, want false for blank-named prompts") + } + }) +} + +// TestJSONRPCErrorErrorAgentCapsBranchcov0719late covers JSONRPCError.Error() across its +// two formatting branches: the populated-message arm (returns the message +// verbatim) and the empty-message / zero-state arm (returns the code-formatted +// fallback). Representative codes from the JSON-RPC error vocabulary are used +// alongside the fully zero value. +func TestJSONRPCErrorErrorAgentCapsBranchcov0719late(t *testing.T) { + cases := []struct { + name string + err JSONRPCError + want string + }{ + { + name: "representative populated message and code returns the message verbatim", + err: JSONRPCError{Code: JSONRPCErrorMethodNotFound, Message: "method not found: tools/call"}, + want: "method not found: tools/call", + }, + { + name: "parse error code with populated message returns the message verbatim", + err: JSONRPCError{Code: JSONRPCErrorParse, Message: "malformed JSON-RPC request: unexpected EOF"}, + want: "malformed JSON-RPC request: unexpected EOF", + }, + { + name: "empty message with representative code returns code-formatted fallback", + err: JSONRPCError{Code: JSONRPCErrorInternal, Message: ""}, + want: "json-rpc error -32603", + }, + { + name: "zero value returns code-formatted fallback with code zero", + err: JSONRPCError{}, + want: "json-rpc error 0", + }, + { + name: "populated message ignores the code entirely", + err: JSONRPCError{Code: JSONRPCErrorInternal, Message: "tools/call handler unavailable"}, + want: "tools/call handler unavailable", + }, + } + + for _, tc := range cases { + t.Run(tc.name, func(t *testing.T) { + got := tc.err.Error() + if got != tc.want { + t.Fatalf("JSONRPCError{Code: %d, Message: %q}.Error() = %q, want %q", tc.err.Code, tc.err.Message, got, tc.want) + } + }) + } +} diff --git a/internal/ai/tools/data_types_branchcov0719late_test.go b/internal/ai/tools/data_types_branchcov0719late_test.go new file mode 100644 index 000000000..bd67972b5 --- /dev/null +++ b/internal/ai/tools/data_types_branchcov0719late_test.go @@ -0,0 +1,271 @@ +package tools + +import ( + "testing" + "time" + + "github.com/stretchr/testify/assert" + "github.com/stretchr/testify/require" +) + +// TestDataTypesBranchcov0719lateDockerHostSummaryNormalize covers +// DockerHostSummary.NormalizeCollections. The method only nil-normalizes +// the Containers slice (it does not sort or dedup); tests assert the actual +// behavior: nil -> empty slice, populated -> unchanged passthrough, empty -> stable. +func TestDataTypesBranchcov0719lateDockerHostSummaryNormalize(t *testing.T) { + t.Run("NilContainersBecomesEmptySlice", func(t *testing.T) { + s := DockerHostSummary{ID: "host-1", Hostname: "dock-1"} + assert.Nil(t, s.Containers, "precondition: input Containers must be nil") + + out := s.NormalizeCollections() + + require.NotNil(t, out.Containers, "nil Containers must become a non-nil empty slice") + assert.Len(t, out.Containers, 0) + assert.Equal(t, "host-1", out.ID, "scalar fields must be preserved") + assert.Equal(t, "dock-1", out.Hostname) + }) + + t.Run("ZeroValueContainersBecomesEmptySlice", func(t *testing.T) { + var s DockerHostSummary + assert.Nil(t, s.Containers) + + out := s.NormalizeCollections() + + require.NotNil(t, out.Containers) + assert.Len(t, out.Containers, 0) + }) + + t.Run("PopulatedContainersPreservedUnchanged", func(t *testing.T) { + // Unsorted, duplicated, multi-element input — NormalizeCollections does + // not sort/dedup, so contents must pass through in original order. + original := []DockerContainerSummary{ + {ID: "c2", Name: "zeta", State: "running"}, + {ID: "c1", Name: "alpha", State: "stopped"}, + {ID: "c1", Name: "alpha", State: "stopped"}, // intentional duplicate + } + s := DockerHostSummary{ + ID: "host-1", + Hostname: "dock-1", + Containers: original, + } + + out := s.NormalizeCollections() + + require.Len(t, out.Containers, 3, "populated slice length must be unchanged") + assert.Equal(t, []DockerContainerSummary(original), out.Containers, + "contents and order must be preserved exactly when not nil") + // Same backing array (no copy expected): identity check on first element addr. + assert.True(t, &out.Containers[0] == &original[0], + "non-nil slice should keep the same backing array (no defensive copy in source)") + }) + + t.Run("EmptyContainersStable", func(t *testing.T) { + s := DockerHostSummary{Containers: []DockerContainerSummary{}} + + out := s.NormalizeCollections() + + require.NotNil(t, out.Containers) + assert.Len(t, out.Containers, 0) + }) +} + +// TestDataTypesBranchcov0719lateK8sNodeSummaryNormalize covers +// K8sNodeSummary.NormalizeCollections. Same shape as Docker: nil Roles +// becomes an empty slice; populated Roles passes through unchanged. +func TestDataTypesBranchcov0719lateK8sNodeSummaryNormalize(t *testing.T) { + t.Run("NilRolesBecomesEmptySlice", func(t *testing.T) { + s := K8sNodeSummary{Name: "n1", Cluster: "c1", Status: "Ready", Ready: true} + assert.Nil(t, s.Roles) + + out := s.NormalizeCollections() + + require.NotNil(t, out.Roles) + assert.Len(t, out.Roles, 0) + assert.Equal(t, "n1", out.Name) + assert.Equal(t, "c1", out.Cluster) + assert.True(t, out.Ready) + }) + + t.Run("ZeroValueRolesBecomesEmptySlice", func(t *testing.T) { + var s K8sNodeSummary + assert.Nil(t, s.Roles) + + out := s.NormalizeCollections() + + require.NotNil(t, out.Roles) + assert.Len(t, out.Roles, 0) + }) + + t.Run("PopulatedRolesPreservedUnchanged", func(t *testing.T) { + // Unsorted + duplicated input; method does not sort/dedup. + original := []string{"worker", "master", "worker", "etcd"} + s := K8sNodeSummary{Name: "n1", Roles: original} + + out := s.NormalizeCollections() + + require.Len(t, out.Roles, 4) + assert.Equal(t, original, out.Roles, "roles must pass through in original order") + }) + + t.Run("EmptyRolesStable", func(t *testing.T) { + s := K8sNodeSummary{Roles: []string{}} + + out := s.NormalizeCollections() + + require.NotNil(t, out.Roles) + assert.Len(t, out.Roles, 0) + }) +} + +// TestDataTypesBranchcov0719latePVEClusterStatusNormalize covers +// PVEClusterStatus.NormalizeCollections. Nil Nodes becomes empty slice; +// populated Nodes passes through unchanged. +func TestDataTypesBranchcov0719latePVEClusterStatusNormalize(t *testing.T) { + t.Run("NilNodesBecomesEmptySlice", func(t *testing.T) { + s := PVEClusterStatus{Instance: "inst-1", ClusterName: "clus-1", QuorumOK: true, TotalNodes: 3} + assert.Nil(t, s.Nodes) + + out := s.NormalizeCollections() + + require.NotNil(t, out.Nodes) + assert.Len(t, out.Nodes, 0) + assert.Equal(t, "inst-1", out.Instance) + assert.Equal(t, "clus-1", out.ClusterName) + assert.True(t, out.QuorumOK) + assert.Equal(t, 3, out.TotalNodes) + }) + + t.Run("ZeroValueNodesBecomesEmptySlice", func(t *testing.T) { + var s PVEClusterStatus + assert.Nil(t, s.Nodes) + + out := s.NormalizeCollections() + + require.NotNil(t, out.Nodes) + assert.Len(t, out.Nodes, 0) + }) + + t.Run("PopulatedNodesPreservedUnchanged", func(t *testing.T) { + // Multi-element, duplicated input — method does not sort/dedup. + original := []PVEClusterNodeStatus{ + {Name: "node-b", Status: "online", IsClusterMember: true}, + {Name: "node-a", Status: "online", IsClusterMember: true}, + {Name: "node-b", Status: "online", IsClusterMember: true}, // duplicate + } + s := PVEClusterStatus{Instance: "inst-1", Nodes: original} + + out := s.NormalizeCollections() + + require.Len(t, out.Nodes, 3) + assert.Equal(t, original, out.Nodes, "nodes must pass through in original order") + }) + + t.Run("EmptyNodesStable", func(t *testing.T) { + s := PVEClusterStatus{Nodes: []PVEClusterNodeStatus{}} + + out := s.NormalizeCollections() + + require.NotNil(t, out.Nodes) + assert.Len(t, out.Nodes, 0) + }) +} + +// TestDataTypesBranchcov0719lateErrExecutionContextUnavailableError covers +// ErrExecutionContextUnavailable.Error(). It returns Message verbatim. +func TestDataTypesBranchcov0719lateErrExecutionContextUnavailableError(t *testing.T) { + t.Run("ReturnsMessageVerbatim", func(t *testing.T) { + const msg = "write would execute on the host node instead of inside the system-container" + err := &ErrExecutionContextUnavailable{ + TargetHost: "homepage-docker", + ResolvedKind: "system-container", + ResolvedNode: "pve-node", + Transport: "direct", + Message: msg, + } + assert.Equal(t, msg, err.Error()) + }) + + t.Run("EmptyMessageYieldsEmptyString", func(t *testing.T) { + err := &ErrExecutionContextUnavailable{} + assert.Equal(t, "", err.Error()) + }) + + t.Run("SatisfiesErrorInterface", func(t *testing.T) { + var err error = &ErrExecutionContextUnavailable{Message: "boom"} + assert.Equal(t, "boom", err.Error()) + }) +} + +// TestDataTypesBranchcov0719lateValidateCurrentResourceAvailable covers +// PulseToolExecutor.ValidateCurrentResourceAvailable across its nil/empty +// error arms and its single-resource OK arm. +func TestDataTypesBranchcov0719lateValidateCurrentResourceAvailable(t *testing.T) { + t.Run("NilResolvedContextReturnsContextError", func(t *testing.T) { + exec := NewPulseToolExecutor(ExecutorConfig{}) + require.Nil(t, exec.GetResolvedContext(), "precondition: no context set") + + err := exec.ValidateCurrentResourceAvailable() + + require.Error(t, err) + assert.Contains(t, err.Error(), "current_resource") + assert.Contains(t, err.Error(), "no Pulse resource context is attached") + }) + + t.Run("EmptyResolvedContextReturnsNoSelectionError", func(t *testing.T) { + exec := NewPulseToolExecutor(ExecutorConfig{}) + exec.SetResolvedContext(&mockResolvedContext{}) + + err := exec.ValidateCurrentResourceAvailable() + + require.Error(t, err) + assert.Contains(t, err.Error(), "current_resource") + assert.Contains(t, err.Error(), "no single attached resource is selected") + }) + + t.Run("SingleResourceReturnsNil", func(t *testing.T) { + res := &mockResource{ + resourceID: "vm:100", + kind: "vm", + targetHost: "vm100", + providerUID: "100", + aliases: []string{"vm100", "100"}, + } + ctx := &mockResolvedContext{ + resources: map[string]ResolvedResourceInfo{ + "vm:100": res, + }, + lastAccessed: map[string]time.Time{ + "vm:100": time.Now(), + }, + } + exec := NewPulseToolExecutor(ExecutorConfig{}) + exec.SetResolvedContext(ctx) + + err := exec.ValidateCurrentResourceAvailable() + + assert.NoError(t, err, "a single attached resource must satisfy validation") + }) + + t.Run("MultipleResourcesReturnsAmbiguousError", func(t *testing.T) { + res1 := &mockResource{resourceID: "vm:100", kind: "vm"} + res2 := &mockResource{resourceID: "vm:101", kind: "vm"} + ctx := &mockResolvedContext{ + resources: map[string]ResolvedResourceInfo{ + "vm:100": res1, + "vm:101": res2, + }, + lastAccessed: map[string]time.Time{ + "vm:100": time.Now(), + "vm:101": time.Now(), + }, + } + exec := NewPulseToolExecutor(ExecutorConfig{}) + exec.SetResolvedContext(ctx) + + err := exec.ValidateCurrentResourceAvailable() + + require.Error(t, err) + assert.Contains(t, err.Error(), "current_resource") + assert.Contains(t, err.Error(), "ambiguous") + }) +} diff --git a/internal/unifiedresources/clone_ceph_branchcov0719late_test.go b/internal/unifiedresources/clone_ceph_branchcov0719late_test.go new file mode 100644 index 000000000..368dc666f --- /dev/null +++ b/internal/unifiedresources/clone_ceph_branchcov0719late_test.go @@ -0,0 +1,382 @@ +package unifiedresources + +import "testing" + +// Branch-coverage tests for the Ceph clone helpers in clone.go. +// +// These exercise the defensive nil/empty arms and the deep-copy +// independence guarantee for the seven Ceph-specific cloners: +// +// - cloneHostCephHealthMeta +// - cloneHostCephMonitorMapMeta +// - cloneHostCephMonitorMetaSlice +// - cloneHostCephPoolMetaSlice +// - cloneHostCephServiceMetaSlice +// - cloneCephPoolMetaSlice +// - cloneCephServiceMetaSlice +// +// For each cloner we assert (a) the nil/empty defensive arm produces a +// safe value without panic, and (b) a populated input clones equal-by-value +// AND remains fully independent when the clone (and any nested slice/map +// element) is mutated. + +// --- cloneHostCephHealthMeta --- + +func TestCloneCephBranchcov0719late_HealthMeta_Empty(t *testing.T) { + in := HostCephHealthMeta{} + out := cloneHostCephHealthMeta(in) + + if out.Status != "" { + t.Errorf("empty input Status: got %q, want empty", out.Status) + } + // cloneHostCephHealthMeta defensively allocates empty maps/slices even + // when the input fields are nil, so callers never need nil-checks. + if out.Checks == nil { + t.Error("empty input should still produce non-nil Checks map") + } + if len(out.Checks) != 0 { + t.Errorf("empty input Checks len: got %d, want 0", len(out.Checks)) + } + if out.Summary == nil { + t.Error("empty input should still produce non-nil Summary slice") + } + if len(out.Summary) != 0 { + t.Errorf("empty input Summary len: got %d, want 0", len(out.Summary)) + } +} + +func TestCloneCephBranchcov0719late_HealthMeta_Isolation(t *testing.T) { + in := HostCephHealthMeta{ + Status: "HEALTH_WARN", + Checks: map[string]HostCephCheckMeta{ + "POOL_NO_REDUNDANCY": { + Severity: "WARNING", + Message: "no redundancy", + Detail: []string{"pool=foo", "min_size=1"}, + }, + }, + Summary: []HostCephHealthSummaryMeta{ + {Severity: "WARNING", Message: "1 pool has no redundancy"}, + }, + } + cloned := cloneHostCephHealthMeta(in) + + // Equal-by-value assertions. + if cloned.Status != "HEALTH_WARN" { + t.Errorf("Status: got %q, want HEALTH_WARN", cloned.Status) + } + if got := cloned.Checks["POOL_NO_REDUNDANCY"].Severity; got != "WARNING" { + t.Errorf("Checks[POOL_NO_REDUNDANCY].Severity: got %q, want WARNING", got) + } + if got := cloned.Checks["POOL_NO_REDUNDANCY"].Message; got != "no redundancy" { + t.Errorf("Checks[POOL_NO_REDUNDANCY].Message: got %q, want 'no redundancy'", got) + } + if got := len(cloned.Checks["POOL_NO_REDUNDANCY"].Detail); got != 2 { + t.Errorf("Checks[POOL_NO_REDUNDANCY].Detail len: got %d, want 2", got) + } + if got := cloned.Checks["POOL_NO_REDUNDANCY"].Detail[0]; got != "pool=foo" { + t.Errorf("Checks[POOL_NO_REDUNDANCY].Detail[0]: got %q, want 'pool=foo'", got) + } + if len(cloned.Summary) != 1 { + t.Fatalf("Summary len: got %d, want 1", len(cloned.Summary)) + } + if cloned.Summary[0].Severity != "WARNING" || cloned.Summary[0].Message != "1 pool has no redundancy" { + t.Errorf("Summary[0]: got %+v, want WARNING/'1 pool has no redundancy'", cloned.Summary[0]) + } + + // Mutate clone's map (add entry) — must not affect original. + cloned.Checks["NEW_CHECK"] = HostCephCheckMeta{Severity: "NEW"} + if _, exists := in.Checks["NEW_CHECK"]; exists { + t.Error("adding key to cloned Checks must not propagate to original") + } + + // Mutate clone's map entry field via copy-out / put-back (required + // because Go disallows direct assignment to a struct field in a map). + c := cloned.Checks["POOL_NO_REDUNDANCY"] + c.Severity = "MUTATED_SEV" + c.Message = "MUTATED_MSG" + c.Detail[0] = "MUTATED_DETAIL" + cloned.Checks["POOL_NO_REDUNDANCY"] = c + + if in.Checks["POOL_NO_REDUNDANCY"].Severity == "MUTATED_SEV" { + t.Error("mutating cloned Checks entry Severity must not propagate to original") + } + if in.Checks["POOL_NO_REDUNDANCY"].Message == "MUTATED_MSG" { + t.Error("mutating cloned Checks entry Message must not propagate to original") + } + if in.Checks["POOL_NO_REDUNDANCY"].Detail[0] == "MUTATED_DETAIL" { + t.Error("mutating cloned Checks entry Detail slice element must not propagate to original") + } + + // Mutate clone's Summary slice — must not affect original. + cloned.Summary[0].Severity = "MUTATED" + cloned.Summary[0].Message = "MUTATED" + if in.Summary[0].Severity == "MUTATED" || in.Summary[0].Message == "MUTATED" { + t.Error("mutating cloned Summary must not propagate to original") + } +} + +// --- cloneHostCephMonitorMapMeta --- + +func TestCloneCephBranchcov0719late_MonMap_Empty(t *testing.T) { + in := HostCephMonitorMapMeta{} + out := cloneHostCephMonitorMapMeta(in) + + if out.Epoch != 0 || out.NumMons != 0 { + t.Errorf("empty input: got Epoch=%d NumMons=%d, want zeros", out.Epoch, out.NumMons) + } + if out.Monitors != nil { + t.Errorf("empty input Monitors: got %v, want nil", out.Monitors) + } +} + +func TestCloneCephBranchcov0719late_MonMap_Isolation(t *testing.T) { + in := HostCephMonitorMapMeta{ + Epoch: 3, + NumMons: 1, + Monitors: []HostCephMonitorMeta{ + {Name: "mon.a", Rank: 0, Addr: "10.0.0.1:6789", Status: "ok"}, + }, + } + cloned := cloneHostCephMonitorMapMeta(in) + + if cloned.Epoch != 3 || cloned.NumMons != 1 { + t.Errorf("scalar fields: got Epoch=%d NumMons=%d, want 3/1", cloned.Epoch, cloned.NumMons) + } + if len(cloned.Monitors) != 1 { + t.Fatalf("Monitors len: got %d, want 1", len(cloned.Monitors)) + } + if cloned.Monitors[0].Name != "mon.a" || cloned.Monitors[0].Addr != "10.0.0.1:6789" { + t.Errorf("Monitors[0]: got %+v, want Name=mon.a Addr=10.0.0.1:6789", cloned.Monitors[0]) + } + + // Mutate clone's Monitors slice — must not affect original. + cloned.Monitors[0].Name = "MUTATED" + cloned.Monitors[0].Status = "DOWN" + if in.Monitors[0].Name == "MUTATED" || in.Monitors[0].Status == "DOWN" { + t.Error("mutating cloned Monitors entry must not propagate to original") + } +} + +// --- cloneHostCephMonitorMetaSlice --- + +func TestCloneCephBranchcov0719late_HostMonSlice_Nil(t *testing.T) { + if got := cloneHostCephMonitorMetaSlice(nil); got != nil { + t.Errorf("nil input: got %v, want nil", got) + } +} + +func TestCloneCephBranchcov0719late_HostMonSlice_Empty(t *testing.T) { + in := []HostCephMonitorMeta{} + out := cloneHostCephMonitorMetaSlice(in) + if out == nil { + t.Error("empty (non-nil) input should produce non-nil empty slice") + } + if len(out) != 0 { + t.Errorf("empty input: got len=%d, want 0", len(out)) + } +} + +func TestCloneCephBranchcov0719late_HostMonSlice_Isolation(t *testing.T) { + in := []HostCephMonitorMeta{ + {Name: "mon.a", Rank: 0, Addr: "10.0.0.1:6789", Status: "ok"}, + {Name: "mon.b", Rank: 1, Addr: "10.0.0.2:6789", Status: "ok"}, + } + cloned := cloneHostCephMonitorMetaSlice(in) + + if len(cloned) != 2 { + t.Fatalf("len: got %d, want 2", len(cloned)) + } + if cloned[0].Name != "mon.a" || cloned[1].Name != "mon.b" { + t.Errorf("values: got %q / %q, want mon.a / mon.b", cloned[0].Name, cloned[1].Name) + } + if cloned[0].Addr != "10.0.0.1:6789" || cloned[1].Rank != 1 { + t.Errorf(" Addr/Rank: got Addr=%q Rank=%d, want 10.0.0.1:6789 / 1", cloned[0].Addr, cloned[1].Rank) + } + + cloned[0].Name = "MUTATED" + cloned[0].Rank = 99 + if in[0].Name == "MUTATED" || in[0].Rank == 99 { + t.Error("mutating cloned element must not propagate to original") + } +} + +// --- cloneHostCephPoolMetaSlice --- + +func TestCloneCephBranchcov0719late_HostPoolSlice_Nil(t *testing.T) { + if got := cloneHostCephPoolMetaSlice(nil); got != nil { + t.Errorf("nil input: got %v, want nil", got) + } +} + +func TestCloneCephBranchcov0719late_HostPoolSlice_Empty(t *testing.T) { + in := []HostCephPoolMeta{} + out := cloneHostCephPoolMetaSlice(in) + if out == nil { + t.Error("empty (non-nil) input should produce non-nil empty slice") + } + if len(out) != 0 { + t.Errorf("empty input: got len=%d, want 0", len(out)) + } +} + +func TestCloneCephBranchcov0719late_HostPoolSlice_Isolation(t *testing.T) { + in := []HostCephPoolMeta{ + {ID: 1, Name: "pool-1", BytesUsed: 1024, BytesAvailable: 2048, Objects: 5, PercentUsed: 0.33}, + {ID: 2, Name: "pool-2", BytesUsed: 4096, BytesAvailable: 8192, Objects: 7, PercentUsed: 0.5}, + } + cloned := cloneHostCephPoolMetaSlice(in) + + if len(cloned) != 2 { + t.Fatalf("len: got %d, want 2", len(cloned)) + } + if cloned[0].Name != "pool-1" || cloned[1].Name != "pool-2" { + t.Errorf("names: got %q / %q, want pool-1 / pool-2", cloned[0].Name, cloned[1].Name) + } + if cloned[0].BytesUsed != 1024 || cloned[1].Objects != 7 { + t.Errorf("numeric values: got BytesUsed=%d Objects=%d, want 1024 / 7", cloned[0].BytesUsed, cloned[1].Objects) + } + + cloned[0].Name = "MUTATED" + cloned[0].BytesUsed = 999 + if in[0].Name == "MUTATED" || in[0].BytesUsed == 999 { + t.Error("mutating cloned element must not propagate to original") + } +} + +// --- cloneHostCephServiceMetaSlice --- + +func TestCloneCephBranchcov0719late_HostSvcSlice_Nil(t *testing.T) { + if got := cloneHostCephServiceMetaSlice(nil); got != nil { + t.Errorf("nil input: got %v, want nil", got) + } +} + +func TestCloneCephBranchcov0719late_HostSvcSlice_Empty(t *testing.T) { + in := []HostCephServiceMeta{} + out := cloneHostCephServiceMetaSlice(in) + if out == nil { + t.Error("empty (non-nil) input should produce non-nil empty slice") + } + if len(out) != 0 { + t.Errorf("empty input: got len=%d, want 0", len(out)) + } +} + +func TestCloneCephBranchcov0719late_HostSvcSlice_Isolation(t *testing.T) { + in := []HostCephServiceMeta{ + {Type: "mon", Running: 3, Total: 3, Daemons: []string{"mon.a", "mon.b", "mon.c"}}, + {Type: "osd", Running: 5, Total: 6, Daemons: []string{"osd.0", "osd.1"}}, + } + cloned := cloneHostCephServiceMetaSlice(in) + + if len(cloned) != 2 { + t.Fatalf("len: got %d, want 2", len(cloned)) + } + if cloned[0].Type != "mon" || cloned[1].Type != "osd" { + t.Errorf("Type: got %q / %q, want mon / osd", cloned[0].Type, cloned[1].Type) + } + if len(cloned[0].Daemons) != 3 || cloned[0].Daemons[0] != "mon.a" { + t.Errorf("Daemons[0]: got %+v, want [mon.a mon.b mon.c]", cloned[0].Daemons) + } + + // Mutate scalar field — must not affect original. + cloned[0].Type = "MUTATED" + cloned[0].Running = 99 + if in[0].Type == "MUTATED" || in[0].Running == 99 { + t.Error("mutating cloned scalar fields must not propagate to original") + } + + // Mutate nested Daemons slice — must not affect original (this is the + // reason cloneHostCephServiceMetaSlice does a per-element Daemons clone). + cloned[0].Daemons[0] = "MUTATED_DAEMON" + if in[0].Daemons[0] == "MUTATED_DAEMON" { + t.Error("mutating cloned nested Daemons slice must not propagate to original") + } +} + +// --- cloneCephPoolMetaSlice (non-Host variant) --- + +func TestCloneCephBranchcov0719late_PoolSlice_Nil(t *testing.T) { + if got := cloneCephPoolMetaSlice(nil); got != nil { + t.Errorf("nil input: got %v, want nil", got) + } +} + +func TestCloneCephBranchcov0719late_PoolSlice_Empty(t *testing.T) { + in := []CephPoolMeta{} + out := cloneCephPoolMetaSlice(in) + if out == nil { + t.Error("empty (non-nil) input should produce non-nil empty slice") + } + if len(out) != 0 { + t.Errorf("empty input: got len=%d, want 0", len(out)) + } +} + +func TestCloneCephBranchcov0719late_PoolSlice_Isolation(t *testing.T) { + in := []CephPoolMeta{ + {Name: "pool-1", StoredBytes: 1024, AvailableBytes: 2048, Objects: 5, PercentUsed: 0.33}, + {Name: "pool-2", StoredBytes: 4096, AvailableBytes: 8192, Objects: 7, PercentUsed: 0.5}, + } + cloned := cloneCephPoolMetaSlice(in) + + if len(cloned) != 2 { + t.Fatalf("len: got %d, want 2", len(cloned)) + } + if cloned[0].Name != "pool-1" || cloned[1].Name != "pool-2" { + t.Errorf("names: got %q / %q, want pool-1 / pool-2", cloned[0].Name, cloned[1].Name) + } + if cloned[0].StoredBytes != 1024 || cloned[1].Objects != 7 { + t.Errorf("numeric values: got StoredBytes=%d Objects=%d, want 1024 / 7", cloned[0].StoredBytes, cloned[1].Objects) + } + + cloned[0].Name = "MUTATED" + cloned[0].StoredBytes = 999 + if in[0].Name == "MUTATED" || in[0].StoredBytes == 999 { + t.Error("mutating cloned element must not propagate to original") + } +} + +// --- cloneCephServiceMetaSlice (non-Host variant) --- + +func TestCloneCephBranchcov0719late_SvcSlice_Nil(t *testing.T) { + if got := cloneCephServiceMetaSlice(nil); got != nil { + t.Errorf("nil input: got %v, want nil", got) + } +} + +func TestCloneCephBranchcov0719late_SvcSlice_Empty(t *testing.T) { + in := []CephServiceMeta{} + out := cloneCephServiceMetaSlice(in) + if out == nil { + t.Error("empty (non-nil) input should produce non-nil empty slice") + } + if len(out) != 0 { + t.Errorf("empty input: got len=%d, want 0", len(out)) + } +} + +func TestCloneCephBranchcov0719late_SvcSlice_Isolation(t *testing.T) { + in := []CephServiceMeta{ + {Type: "mon", Running: 3, Total: 3}, + {Type: "osd", Running: 5, Total: 6}, + } + cloned := cloneCephServiceMetaSlice(in) + + if len(cloned) != 2 { + t.Fatalf("len: got %d, want 2", len(cloned)) + } + if cloned[0].Type != "mon" || cloned[1].Type != "osd" { + t.Errorf("Type: got %q / %q, want mon / osd", cloned[0].Type, cloned[1].Type) + } + if cloned[0].Running != 3 || cloned[1].Total != 6 { + t.Errorf("values: got Running=%d Total=%d, want 3 / 6", cloned[0].Running, cloned[1].Total) + } + + cloned[0].Type = "MUTATED" + cloned[0].Running = 99 + if in[0].Type == "MUTATED" || in[0].Running == 99 { + t.Error("mutating cloned element must not propagate to original") + } +} diff --git a/internal/unifiedresources/dockercontainerview_branchcov0719late_test.go b/internal/unifiedresources/dockercontainerview_branchcov0719late_test.go new file mode 100644 index 000000000..9c632d06d --- /dev/null +++ b/internal/unifiedresources/dockercontainerview_branchcov0719late_test.go @@ -0,0 +1,372 @@ +package unifiedresources + +import ( + "reflect" + "testing" + "time" +) + +// TestDockerContainerViewBranchcov0719late covers every requested +// DockerContainerView accessor across all three relevant arms: +// - the nil-receiver arm (v.r == nil) +// - the nil-nested arm (v.r != nil but v.r.Docker == nil) +// - the fully-populated arm (with cloned slice/map/ptr independence checks) +// +// The empty-nested arm additionally exercises the slice/map/ptr fields when +// the *DockerData payload exists but the nested collections are unset/empty. +func TestDockerContainerViewBranchcov0719late(t *testing.T) { + now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC) + parentID := "docker-host-1" + + r := &Resource{ + ID: "app-container-1", + Type: ResourceTypeAppContainer, + Name: "nextcloud", + Status: StatusOnline, + LastSeen: now, + Tags: []string{"app", "tier:web"}, + ParentID: &parentID, + Docker: &DockerData{ + HostSourceID: " docker-host-src-1 ", + ContainerID: "container-123", + Image: "docker.io/library/nextcloud:29", + ContainerState: "running", + Health: "healthy", + RestartCount: 3, + ExitCode: 137, + UptimeSeconds: 9999, + Ports: []DockerPortMeta{ + {PrivatePort: 80, PublicPort: 8080, Protocol: "tcp", IP: "0.0.0.0"}, + }, + Labels: map[string]string{"com.example.stack": "web"}, + Networks: []DockerNetworkMeta{{Name: "frontend", IPv4: "172.20.0.2", IPv6: "fd00::2"}}, + Mounts: []DockerMountMeta{{Type: "volume", Source: "data", Destination: "/var/www/html", Mode: "rw", RW: true}}, + UpdateStatus: &DockerUpdateStatusMeta{ + UpdateAvailable: true, + CurrentDigest: "sha256:abc", + LatestDigest: "sha256:def", + LastChecked: now, + Error: "", + }, + }, + Metrics: &ResourceMetrics{ + CPU: &MetricValue{Percent: 42.5}, + Memory: &MetricValue{Used: ptrInt64(512), Total: ptrInt64(1024), Percent: 50}, + Disk: &MetricValue{Percent: 11}, + NetIn: &MetricValue{Value: 1234.5}, + NetOut: &MetricValue{Value: 6789.0}, + }, + } + + v := NewDockerContainerView(r) + + t.Run("String_populated", func(t *testing.T) { + if got, want := v.String(), `DockerContainerView(app-container-1, "nextcloud")`; got != want { + t.Fatalf("expected %q, got %q", want, got) + } + }) + + t.Run("Status_populated", func(t *testing.T) { + if got := v.Status(); got != StatusOnline { + t.Fatalf("expected Status %q, got %q", StatusOnline, got) + } + }) + + t.Run("HostSourceID_populated_trims", func(t *testing.T) { + if got, want := v.HostSourceID(), "docker-host-src-1"; got != want { + t.Fatalf("expected HostSourceID %q, got %q", want, got) + } + }) + + t.Run("Image_populated", func(t *testing.T) { + if got, want := v.Image(), "docker.io/library/nextcloud:29"; got != want { + t.Fatalf("expected Image %q, got %q", want, got) + } + }) + + t.Run("ContainerState_populated", func(t *testing.T) { + if got, want := v.ContainerState(), "running"; got != want { + t.Fatalf("expected ContainerState %q, got %q", want, got) + } + }) + + t.Run("Health_populated", func(t *testing.T) { + if got, want := v.Health(), "healthy"; got != want { + t.Fatalf("expected Health %q, got %q", want, got) + } + }) + + t.Run("RestartCount_populated", func(t *testing.T) { + if got, want := v.RestartCount(), 3; got != want { + t.Fatalf("expected RestartCount %d, got %d", want, got) + } + }) + + t.Run("ExitCode_populated", func(t *testing.T) { + if got, want := v.ExitCode(), 137; got != want { + t.Fatalf("expected ExitCode %d, got %d", want, got) + } + }) + + t.Run("CPUPercent_populated", func(t *testing.T) { + if got, want := v.CPUPercent(), 42.5; got != want { + t.Fatalf("expected CPUPercent %v, got %v", want, got) + } + }) + + t.Run("MemoryUsed_populated", func(t *testing.T) { + if got, want := v.MemoryUsed(), int64(512); got != want { + t.Fatalf("expected MemoryUsed %d, got %d", want, got) + } + }) + + t.Run("MemoryTotal_populated", func(t *testing.T) { + if got, want := v.MemoryTotal(), int64(1024); got != want { + t.Fatalf("expected MemoryTotal %d, got %d", want, got) + } + }) + + t.Run("MemoryPercent_populated", func(t *testing.T) { + if got, want := v.MemoryPercent(), 50.0; got != want { + t.Fatalf("expected MemoryPercent %v, got %v", want, got) + } + }) + + t.Run("DiskPercent_populated", func(t *testing.T) { + if got, want := v.DiskPercent(), 11.0; got != want { + t.Fatalf("expected DiskPercent %v, got %v", want, got) + } + }) + + t.Run("NetInRate_populated", func(t *testing.T) { + if got, want := v.NetInRate(), 1234.5; got != want { + t.Fatalf("expected NetInRate %v, got %v", want, got) + } + }) + + t.Run("NetOutRate_populated", func(t *testing.T) { + if got, want := v.NetOutRate(), 6789.0; got != want { + t.Fatalf("expected NetOutRate %v, got %v", want, got) + } + }) + + t.Run("UptimeSeconds_populated", func(t *testing.T) { + if got, want := v.UptimeSeconds(), int64(9999); got != want { + t.Fatalf("expected UptimeSeconds %d, got %d", want, got) + } + }) + + t.Run("Ports_populated_and_cloned", func(t *testing.T) { + got := v.Ports() + if len(got) != 1 || got[0].PrivatePort != 80 || got[0].PublicPort != 8080 || got[0].Protocol != "tcp" || got[0].IP != "0.0.0.0" { + t.Fatalf("expected populated port meta, got %+v", got) + } + if !reflect.DeepEqual(got, r.Docker.Ports) { + t.Fatalf("Ports clone should equal source by value: got %+v want %+v", got, r.Docker.Ports) + } + got[0].PrivatePort = 9999 + if again := v.Ports(); len(again) != 1 || again[0].PrivatePort != 80 { + t.Fatalf("Ports() must return an independent slice, got %+v", again) + } + }) + + t.Run("Labels_populated_and_cloned", func(t *testing.T) { + got := v.Labels() + if len(got) != 1 || got["com.example.stack"] != "web" { + t.Fatalf("expected populated labels, got %+v", got) + } + if !reflect.DeepEqual(got, r.Docker.Labels) { + t.Fatalf("Labels clone should equal source by value: got %+v want %+v", got, r.Docker.Labels) + } + got["com.example.stack"] = "mutated" + got["injected"] = "x" + if again := v.Labels(); len(again) != 1 || again["com.example.stack"] != "web" { + t.Fatalf("Labels() must return an independent map, got %+v", again) + } + }) + + t.Run("Networks_populated_and_cloned", func(t *testing.T) { + got := v.Networks() + if len(got) != 1 || got[0].Name != "frontend" || got[0].IPv4 != "172.20.0.2" || got[0].IPv6 != "fd00::2" { + t.Fatalf("expected populated network meta, got %+v", got) + } + if !reflect.DeepEqual(got, r.Docker.Networks) { + t.Fatalf("Networks clone should equal source by value: got %+v want %+v", got, r.Docker.Networks) + } + got[0].Name = "mutated" + if again := v.Networks(); len(again) != 1 || again[0].Name != "frontend" { + t.Fatalf("Networks() must return an independent slice, got %+v", again) + } + }) + + t.Run("Mounts_populated_and_cloned", func(t *testing.T) { + got := v.Mounts() + if len(got) != 1 || got[0].Type != "volume" || got[0].Source != "data" || got[0].Destination != "/var/www/html" || !got[0].RW { + t.Fatalf("expected populated mount meta, got %+v", got) + } + if !reflect.DeepEqual(got, r.Docker.Mounts) { + t.Fatalf("Mounts clone should equal source by value: got %+v want %+v", got, r.Docker.Mounts) + } + got[0].Source = "mutated" + if again := v.Mounts(); len(again) != 1 || again[0].Source != "data" { + t.Fatalf("Mounts() must return an independent slice, got %+v", again) + } + }) + + t.Run("UpdateStatus_populated_and_cloned", func(t *testing.T) { + got := v.UpdateStatus() + if got == nil || !got.UpdateAvailable || got.CurrentDigest != "sha256:abc" || got.LatestDigest != "sha256:def" || !got.LastChecked.Equal(now) { + t.Fatalf("expected populated update status, got %+v", got) + } + got.UpdateAvailable = false + got.CurrentDigest = "mutated" + if again := v.UpdateStatus(); again == nil || !again.UpdateAvailable || again.CurrentDigest != "sha256:abc" { + t.Fatalf("UpdateStatus() must return an independent copy, got %+v", again) + } + }) + + t.Run("Tags_populated_and_cloned", func(t *testing.T) { + got := v.Tags() + assertStringSlice(t, got, []string{"app", "tier:web"}) + got[0] = "mutated" + if again := v.Tags(); len(again) != 2 || again[0] != "app" { + t.Fatalf("Tags() must return an independent slice, got %+v", again) + } + }) + + t.Run("LastSeen_populated", func(t *testing.T) { + if got := v.LastSeen(); !got.Equal(now) { + t.Fatalf("expected LastSeen %v, got %v", now, got) + } + }) + + t.Run("NilNestedDockerArm", func(t *testing.T) { + r2 := &Resource{ + ID: "container-no-docker", + Type: ResourceTypeAppContainer, + Name: "stub", + Status: StatusOffline, + LastSeen: now, + Tags: []string{"t1"}, + Docker: nil, + Metrics: &ResourceMetrics{ + CPU: &MetricValue{Percent: 1}, + Memory: &MetricValue{Used: ptrInt64(2), Total: ptrInt64(4), Percent: 50}, + Disk: &MetricValue{Percent: 5}, + NetIn: &MetricValue{Value: 6}, + NetOut: &MetricValue{Value: 7}, + }, + } + v2 := NewDockerContainerView(r2) + + if v2.ID() != "container-no-docker" || v2.Name() != "stub" || v2.Status() != StatusOffline { + t.Fatalf("basic accessors should still reflect resource fields, got id=%q name=%q status=%q", v2.ID(), v2.Name(), v2.Status()) + } + if got := v2.String(); got != `DockerContainerView(container-no-docker, "stub")` { + t.Fatalf("expected String %q, got %q", `DockerContainerView(container-no-docker, "stub")`, got) + } + if v2.HostSourceID() != "" { + t.Fatalf("expected empty HostSourceID when Docker is nil, got %q", v2.HostSourceID()) + } + if v2.Image() != "" { + t.Fatalf("expected empty Image when Docker is nil, got %q", v2.Image()) + } + if v2.ContainerState() != "" { + t.Fatalf("expected empty ContainerState when Docker is nil, got %q", v2.ContainerState()) + } + if v2.Health() != "" { + t.Fatalf("expected empty Health when Docker is nil, got %q", v2.Health()) + } + if v2.RestartCount() != 0 { + t.Fatalf("expected zero RestartCount when Docker is nil, got %d", v2.RestartCount()) + } + if v2.ExitCode() != 0 { + t.Fatalf("expected zero ExitCode when Docker is nil, got %d", v2.ExitCode()) + } + if v2.UptimeSeconds() != 0 { + t.Fatalf("expected zero UptimeSeconds when Docker is nil, got %d", v2.UptimeSeconds()) + } + if v2.Ports() != nil { + t.Fatalf("expected nil Ports when Docker is nil, got %+v", v2.Ports()) + } + if v2.Labels() != nil { + t.Fatalf("expected nil Labels when Docker is nil, got %+v", v2.Labels()) + } + if v2.Networks() != nil { + t.Fatalf("expected nil Networks when Docker is nil, got %+v", v2.Networks()) + } + if v2.Mounts() != nil { + t.Fatalf("expected nil Mounts when Docker is nil, got %+v", v2.Mounts()) + } + if v2.UpdateStatus() != nil { + t.Fatalf("expected nil UpdateStatus when Docker is nil, got %+v", v2.UpdateStatus()) + } + if v2.CPUPercent() != 1 || v2.MemoryUsed() != 2 || v2.MemoryTotal() != 4 || v2.MemoryPercent() != 50 || v2.DiskPercent() != 5 || v2.NetInRate() != 6 || v2.NetOutRate() != 7 { + t.Fatalf("metric accessors must still resolve via r.Metrics when Docker is nil, got cpu=%v memUsed=%d memTotal=%d memPct=%v diskPct=%v netIn=%v netOut=%v", v2.CPUPercent(), v2.MemoryUsed(), v2.MemoryTotal(), v2.MemoryPercent(), v2.DiskPercent(), v2.NetInRate(), v2.NetOutRate()) + } + if !v2.LastSeen().Equal(now) { + t.Fatalf("expected LastSeen %v, got %v", now, v2.LastSeen()) + } + assertStringSlice(t, v2.Tags(), []string{"t1"}) + }) + + t.Run("EmptyNestedCollections", func(t *testing.T) { + r3 := &Resource{ + ID: "container-empty-collections", + Type: ResourceTypeAppContainer, + Name: "empty", + Status: StatusOnline, + Docker: &DockerData{ + ContainerID: "container-456", + Ports: []DockerPortMeta{}, + Labels: map[string]string{}, + Networks: []DockerNetworkMeta{}, + Mounts: []DockerMountMeta{}, + UpdateStatus: nil, + }, + } + v3 := NewDockerContainerView(r3) + + if got := v3.Ports(); got == nil || len(got) != 0 { + t.Fatalf("expected non-nil empty Ports for empty input, got %+v", got) + } + if got := v3.Labels(); got == nil || len(got) != 0 { + t.Fatalf("expected non-nil empty Labels for empty input, got %+v", got) + } + if got := v3.Networks(); got == nil || len(got) != 0 { + t.Fatalf("expected non-nil empty Networks for empty input, got %+v", got) + } + if got := v3.Mounts(); got == nil || len(got) != 0 { + t.Fatalf("expected non-nil empty Mounts for empty input, got %+v", got) + } + if got := v3.UpdateStatus(); got != nil { + t.Fatalf("expected nil UpdateStatus when nested ptr is nil, got %+v", got) + } + }) + + t.Run("NilReceiverArm", func(t *testing.T) { + var zero DockerContainerView + + if got := zero.String(); got != `DockerContainerView(, "")` { + t.Fatalf("expected String %q for nil receiver, got %q", `DockerContainerView(, "")`, got) + } + if zero.ID() != "" || zero.Name() != "" || zero.Status() != "" { + t.Fatalf("expected empty ID/Name/Status for nil receiver, got id=%q name=%q status=%q", zero.ID(), zero.Name(), zero.Status()) + } + if zero.HostSourceID() != "" || zero.Image() != "" || zero.ContainerState() != "" || zero.Health() != "" || zero.RestartCount() != 0 || zero.ExitCode() != 0 || zero.UptimeSeconds() != 0 { + t.Fatalf("expected docker accessors to return zero values for nil receiver, got source=%q image=%q state=%q health=%q restarts=%d exit=%d uptime=%d", zero.HostSourceID(), zero.Image(), zero.ContainerState(), zero.Health(), zero.RestartCount(), zero.ExitCode(), zero.UptimeSeconds()) + } + if zero.CPUPercent() != 0 || zero.MemoryUsed() != 0 || zero.MemoryTotal() != 0 || zero.MemoryPercent() != 0 || zero.DiskPercent() != 0 || zero.NetInRate() != 0 || zero.NetOutRate() != 0 { + t.Fatalf("expected metric accessors to return zero values for nil receiver, got cpu=%v memUsed=%d memTotal=%d memPct=%v diskPct=%v netIn=%v netOut=%v", zero.CPUPercent(), zero.MemoryUsed(), zero.MemoryTotal(), zero.MemoryPercent(), zero.DiskPercent(), zero.NetInRate(), zero.NetOutRate()) + } + if zero.Ports() != nil || zero.Labels() != nil || zero.Networks() != nil || zero.Mounts() != nil || zero.UpdateStatus() != nil { + t.Fatalf("expected nil slice/map/ptr accessors for nil receiver, got ports=%v labels=%v networks=%v mounts=%v update=%v", zero.Ports(), zero.Labels(), zero.Networks(), zero.Mounts(), zero.UpdateStatus()) + } + if zero.Tags() != nil { + t.Fatalf("expected nil Tags for nil receiver, got %v", zero.Tags()) + } + if !zero.LastSeen().IsZero() { + t.Fatalf("expected zero LastSeen for nil receiver, got %v", zero.LastSeen()) + } + }) +} diff --git a/internal/unifiedresources/hostviews_branchcov0719late_test.go b/internal/unifiedresources/hostviews_branchcov0719late_test.go new file mode 100644 index 000000000..7667e243d --- /dev/null +++ b/internal/unifiedresources/hostviews_branchcov0719late_test.go @@ -0,0 +1,603 @@ +package unifiedresources + +import ( + "reflect" + "testing" + "time" + + "github.com/rcourtman/pulse-go-rewrite/internal/models" + "github.com/rcourtman/pulse-go-rewrite/internal/storagehealth" +) + +// ptrTime is a small helper local to this branch-coverage file. +func ptrTime(v time.Time) *time.Time { return &v } + +// ===================== +// HostView accessors +// ===================== + +// HostView.TokenLastUsedAt has a triple nil-guard and returns an independent +// *time.Time when populated. +func TestHostViewsBranchcov0719late_HostTokenLastUsedAt(t *testing.T) { + // (a) nil receiver. + var zero HostView + if got := zero.TokenLastUsedAt(); got != nil { + t.Fatalf("nil receiver: expected nil, got %v", got) + } + + // (a) nil Agent. + r := &Resource{ID: "h-tlu-1", Type: ResourceTypeAgent} + if got := NewHostView(r).TokenLastUsedAt(); got != nil { + t.Fatalf("nil Agent: expected nil, got %v", got) + } + + // (a) Agent set but TokenLastUsedAt pointer nil. + r.Agent = &AgentData{} + if got := NewHostView(r).TokenLastUsedAt(); got != nil { + t.Fatalf("nil TokenLastUsedAt: expected nil, got %v", got) + } + + // (b) Populated: equal value via an independent pointer. + used := time.Date(2026, 7, 19, 10, 0, 0, 0, time.UTC) + r.Agent.TokenLastUsedAt = ptrTime(used) + got := NewHostView(r).TokenLastUsedAt() + if got == nil { + t.Fatal("populated: expected non-nil") + } + if !got.Equal(used) { + t.Fatalf("expected %v, got %v", used, *got) + } + // Mutating the returned pointer's pointee must not leak back to the source. + *got = got.Add(time.Hour) + if !r.Agent.TokenLastUsedAt.Equal(used) { + t.Fatalf("mutation leaked to source: source=%v want=%v", *r.Agent.TokenLastUsedAt, used) + } +} + +// HostView.PackageUpdates guards nil receiver / nil Agent / nil PackageUpdates +// and returns a clone with an independent Packages slice when populated. +func TestHostViewsBranchcov0719late_HostPackageUpdates(t *testing.T) { + // (a) nil receiver. + var zero HostView + if got := zero.PackageUpdates(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Agent. + r := &Resource{ID: "h-pkg-1", Type: ResourceTypeAgent} + if got := NewHostView(r).PackageUpdates(); got != nil { + t.Fatalf("nil Agent: expected nil, got %+v", got) + } + // (a) Agent present but PackageUpdates pointer nil. + r.Agent = &AgentData{} + if got := NewHostView(r).PackageUpdates(); got != nil { + t.Fatalf("nil PackageUpdates: expected nil, got %+v", got) + } + // (b) Populated: value-equal copy with an independent Packages slice. + checked := time.Date(2026, 7, 19, 9, 0, 0, 0, time.UTC) + r.Agent.PackageUpdates = &AgentPackageUpdateMeta{ + Supported: true, + Manager: "apt", + InventoryHash: "abc", + PendingCount: 2, + CheckedAt: checked, + Packages: []AgentPackageUpdate{ + {Name: "curl", InstalledVersion: "1.0", AvailableVersion: "1.1"}, + {Name: "wget", InstalledVersion: "2.0", AvailableVersion: "2.1"}, + }, + RebootRequired: true, + } + got := NewHostView(r).PackageUpdates() + if got == nil { + t.Fatal("populated: expected non-nil") + } + if !reflect.DeepEqual(*got, *r.Agent.PackageUpdates) { + t.Fatalf("expected value-equal clone, got %+v want %+v", *got, *r.Agent.PackageUpdates) + } + // Mutate scalar on the clone; source unaffected. + got.Manager = "yum" + if r.Agent.PackageUpdates.Manager != "apt" { + t.Fatalf("scalar mutation leaked to source: source.Manager=%q want %q", r.Agent.PackageUpdates.Manager, "apt") + } + // Mutate element of Packages; source unaffected. + got.Packages[0].Name = "mutated" + if r.Agent.PackageUpdates.Packages[0].Name != "curl" { + t.Fatalf("Packages element mutation leaked: source[0].Name=%q want %q", r.Agent.PackageUpdates.Packages[0].Name, "curl") + } + // Append to Packages; source length unchanged. + got.Packages = append(got.Packages, AgentPackageUpdate{Name: "extra"}) + if len(r.Agent.PackageUpdates.Packages) != 2 { + t.Fatalf("Packages append leaked to source: source len=%d want 2", len(r.Agent.PackageUpdates.Packages)) + } +} + +// HostView.StorageCleanup guards nil receiver / nil Agent / nil StorageCleanup. +func TestHostViewsBranchcov0719late_HostStorageCleanup(t *testing.T) { + // (a) nil receiver. + var zero HostView + if got := zero.StorageCleanup(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Agent. + r := &Resource{ID: "h-cln-1", Type: ResourceTypeAgent} + if got := NewHostView(r).StorageCleanup(); got != nil { + t.Fatalf("nil Agent: expected nil, got %+v", got) + } + // (a) Agent present but StorageCleanup pointer nil. + r.Agent = &AgentData{} + if got := NewHostView(r).StorageCleanup(); got != nil { + t.Fatalf("nil StorageCleanup: expected nil, got %+v", got) + } + // (b) Populated: value-equal shallow copy. + checked := time.Date(2026, 7, 19, 8, 0, 0, 0, time.UTC) + r.Agent.StorageCleanup = &AgentStorageCleanupMeta{ + Supported: true, + Provider: "journal", + Fingerprint: "fp-1", + ReclaimableBytes: 1024, + CheckedAt: checked, + } + got := NewHostView(r).StorageCleanup() + if got == nil { + t.Fatal("populated: expected non-nil") + } + if !reflect.DeepEqual(*got, *r.Agent.StorageCleanup) { + t.Fatalf("expected value-equal clone, got %+v want %+v", *got, *r.Agent.StorageCleanup) + } + got.Provider = "apt" + if r.Agent.StorageCleanup.Provider != "journal" { + t.Fatalf("scalar mutation leaked to source: source.Provider=%q want %q", r.Agent.StorageCleanup.Provider, "journal") + } +} + +// HostView.Capabilities appends into a fresh slice from r.Capabilities. +func TestHostViewsBranchcov0719late_HostCapabilities(t *testing.T) { + // (a) nil receiver. + var zero HostView + if got := zero.Capabilities(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) Resource present, no capabilities set: append of nil yields nil. + r := &Resource{ID: "h-cap-1", Type: ResourceTypeAgent} + if got := NewHostView(r).Capabilities(); got != nil { + t.Fatalf("empty source capabilities: expected nil, got %+v", got) + } + // (b) Populated: equal contents with an independent backing array. + r.Capabilities = []ResourceCapability{ + {Name: "restart", InternalHandler: "agent.restart"}, + {Name: "shell", InternalHandler: "agent.shell"}, + } + got := NewHostView(r).Capabilities() + if len(got) != 2 || got[0].Name != "restart" || got[1].InternalHandler != "agent.shell" { + t.Fatalf("expected cloned capabilities, got %+v", got) + } + // Mutating element value fields on the clone must not affect the source. + got[0].Name = "mutated" + got[1].InternalHandler = "leaked" + if r.Capabilities[0].Name != "restart" || r.Capabilities[1].InternalHandler != "agent.shell" { + t.Fatalf("mutation leaked to source: %+v", r.Capabilities) + } + // Appending must not grow the source's backing array. + got = append(got, ResourceCapability{Name: "extra"}) + if len(r.Capabilities) != 2 { + t.Fatalf("append leaked to source: source len=%d want 2", len(r.Capabilities)) + } +} + +// HostView.RAID clones a slice of HostRAIDMeta (with nested Devices + Risk). +func TestHostViewsBranchcov0719late_HostRAID(t *testing.T) { + // (a) nil receiver. + var zero HostView + if got := zero.RAID(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Agent. + r := &Resource{ID: "h-raid-1", Type: ResourceTypeAgent} + if got := NewHostView(r).RAID(); got != nil { + t.Fatalf("nil Agent: expected nil, got %+v", got) + } + // (a) Agent present, RAID nil: cloneHostRAIDMetaSlice(nil) returns nil. + r.Agent = &AgentData{} + if got := NewHostView(r).RAID(); got != nil { + t.Fatalf("nil RAID slice: expected nil, got %+v", got) + } + // (b) Populated: deep clone with independent nested Devices/Risk. + r.Agent.RAID = []HostRAIDMeta{ + { + Device: "md0", + Level: "raid1", + State: "active", + TotalDevices: 2, + ActiveDevices: 2, + Devices: []HostRAIDDeviceMeta{ + {Device: "sda", State: "active", Slot: 0}, + {Device: "sdb", State: "active", Slot: 1}, + }, + Risk: &StorageRisk{ + Level: storagehealth.RiskWarning, + Reasons: []StorageRiskReason{{Code: "raid_degraded", Severity: storagehealth.RiskWarning, Summary: "RAID degraded"}}, + }, + }, + } + got := NewHostView(r).RAID() + if len(got) != 1 || got[0].Device != "md0" || len(got[0].Devices) != 2 || got[0].Risk == nil { + t.Fatalf("expected deep-cloned RAID slice, got %+v", got) + } + // Mutating a nested Devices element must not leak to source. + got[0].Devices[0].Device = "mutated" + if r.Agent.RAID[0].Devices[0].Device != "sda" { + t.Fatalf("nested Devices mutation leaked: source[0].Devices[0].Device=%q want %q", r.Agent.RAID[0].Devices[0].Device, "sda") + } + // Truncating nested Devices on the clone must not affect source length. + got[0].Devices = got[0].Devices[:0] + if len(r.Agent.RAID[0].Devices) != 2 { + t.Fatalf("Devices truncation leaked: source len=%d want 2", len(r.Agent.RAID[0].Devices)) + } + // Mutating nested Risk.Reasons must not leak to source. + got[0].Risk.Reasons[0].Code = "leaked" + if r.Agent.RAID[0].Risk.Reasons[0].Code != "raid_degraded" { + t.Fatalf("Risk.Reasons mutation leaked: source code=%q want %q", r.Agent.RAID[0].Risk.Reasons[0].Code, "raid_degraded") + } + // Re-clone to verify Risk pointer is a fresh allocation (not shared). + got2 := NewHostView(r).RAID() + if &got2[0].Risk == &r.Agent.RAID[0].Risk { + t.Fatal("expected Risk pointer to be a fresh allocation, not shared with source") + } +} + +// HostView.DiskIO clones a slice of HostDiskIOMeta (flat struct). +func TestHostViewsBranchcov0719late_HostDiskIO(t *testing.T) { + // (a) nil receiver. + var zero HostView + if got := zero.DiskIO(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Agent. + r := &Resource{ID: "h-dio-1", Type: ResourceTypeAgent} + if got := NewHostView(r).DiskIO(); got != nil { + t.Fatalf("nil Agent: expected nil, got %+v", got) + } + // (a) Agent present, DiskIO nil. + r.Agent = &AgentData{} + if got := NewHostView(r).DiskIO(); got != nil { + t.Fatalf("nil DiskIO slice: expected nil, got %+v", got) + } + // (b) Populated: fresh slice with equal contents. + r.Agent.DiskIO = []HostDiskIOMeta{ + {Device: "sda", ReadBytes: 100, WriteBytes: 200, ReadOps: 1, WriteOps: 2}, + {Device: "sdb", ReadBytes: 300, WriteBytes: 400, ReadOps: 3, WriteOps: 4}, + } + got := NewHostView(r).DiskIO() + if len(got) != 2 || got[0].Device != "sda" || got[1].WriteOps != 4 { + t.Fatalf("expected cloned DiskIO slice, got %+v", got) + } + // Mutate element fields on clone; source unaffected (fresh backing array). + got[0].Device = "mutated" + got[0].ReadBytes = 999 + if r.Agent.DiskIO[0].Device != "sda" || r.Agent.DiskIO[0].ReadBytes != 100 { + t.Fatalf("mutation leaked to source: %+v", r.Agent.DiskIO[0]) + } + // Append on clone must not affect source. + got = append(got, HostDiskIOMeta{Device: "extra"}) + if len(r.Agent.DiskIO) != 2 { + t.Fatalf("append leaked to source: source len=%d want 2", len(r.Agent.DiskIO)) + } +} + +// HostView.Ceph clones *HostCephMeta (with nested maps/slices). +func TestHostViewsBranchcov0719late_HostCeph(t *testing.T) { + // (a) nil receiver. + var zero HostView + if got := zero.Ceph(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Agent. + r := &Resource{ID: "h-ceph-1", Type: ResourceTypeAgent} + if got := NewHostView(r).Ceph(); got != nil { + t.Fatalf("nil Agent: expected nil, got %+v", got) + } + // (a) Agent present, Ceph nil: cloneHostCephMeta(nil) returns nil. + r.Agent = &AgentData{} + if got := NewHostView(r).Ceph(); got != nil { + t.Fatalf("nil Ceph: expected nil, got %+v", got) + } + // (b) Populated: deep clone with independent nested maps/slices. + r.Agent.Ceph = &HostCephMeta{ + FSID: "ceph-fsid-1", + HealthStatus: "HEALTH_WARN", + Health: HostCephHealthMeta{ + Status: "HEALTH_WARN", + Checks: map[string]HostCephCheckMeta{ + "POOL_NEAR_FULL": {Severity: "warning", Message: "pool near full", Detail: []string{"pool1"}}, + }, + Summary: []HostCephHealthSummaryMeta{{Severity: "warning", Message: "near full"}}, + }, + Pools: []HostCephPoolMeta{{ID: 1, Name: "pool1", PercentUsed: 0.9}}, + Services: []HostCephServiceMeta{ + {Type: "mon", Running: 1, Total: 1, Daemons: []string{"mon.a"}}, + }, + } + got := NewHostView(r).Ceph() + if got == nil || got.FSID != "ceph-fsid-1" || got.HealthStatus != "HEALTH_WARN" { + t.Fatalf("expected cloned ceph meta, got %+v", got) + } + if got.Health.Checks == nil || got.Health.Checks["POOL_NEAR_FULL"].Message != "pool near full" { + t.Fatalf("expected cloned checks map, got %+v", got.Health.Checks) + } + // Mutating the nested map on the clone must not leak to source. + got.Health.Checks["POOL_NEAR_FULL"] = HostCephCheckMeta{Severity: "error", Message: "mutated"} + if r.Agent.Ceph.Health.Checks["POOL_NEAR_FULL"].Message != "pool near full" { + t.Fatalf("Health.Checks mutation leaked: %q", r.Agent.Ceph.Health.Checks["POOL_NEAR_FULL"].Message) + } + // Adding a new key on clone must not affect source map. + got.Health.Checks["NEW"] = HostCephCheckMeta{} + if len(r.Agent.Ceph.Health.Checks) != 1 { + t.Fatalf("map addition leaked: source len=%d want 1", len(r.Agent.Ceph.Health.Checks)) + } + // Mutating nested Detail slice via a fresh clone must not leak to source. + fresh := NewHostView(r).Ceph() + fresh.Health.Checks["POOL_NEAR_FULL"].Detail[0] = "leaked" + if r.Agent.Ceph.Health.Checks["POOL_NEAR_FULL"].Detail[0] != "pool1" { + t.Fatalf("Detail mutation leaked: %q", r.Agent.Ceph.Health.Checks["POOL_NEAR_FULL"].Detail[0]) + } + // Mutating Pools slice element on clone must not affect source. + got.Pools[0].Name = "leaked" + if r.Agent.Ceph.Pools[0].Name != "pool1" { + t.Fatalf("Pools mutation leaked: %q", r.Agent.Ceph.Pools[0].Name) + } + // Mutating nested Services[].Daemons must not leak to source. + got.Services[0].Daemons[0] = "leaked" + if r.Agent.Ceph.Services[0].Daemons[0] != "mon.a" { + t.Fatalf("Services.Daemons mutation leaked: %q", r.Agent.Ceph.Services[0].Daemons[0]) + } +} + +// ===================== +// DockerHostView accessors +// ===================== + +// dockerHostFixture returns a Docker host Resource with fully-populated swarm +// collections plus the security/hidden flags used by the accessors under test. +func dockerHostFixture() *Resource { + return &Resource{ + ID: "dh-1", + Type: ResourceTypeAgent, + Name: "dh-name", + Docker: &DockerData{ + Hidden: true, + Security: &models.DockerHostSecurity{ + AuthorizationPlugins: []string{"authz1", "authz2"}, + MutatingCommandsBlocked: true, + MutatingCommandsBlockedReason: "policy", + }, + Services: []models.DockerService{{ID: "svc-1", Name: "nginx", Stack: "web"}}, + Tasks: []models.DockerTask{{ID: "task-1", ServiceID: "svc-1", NodeID: "node-1"}}, + Nodes: []models.DockerNode{{ID: "node-1", Hostname: "n1", Role: "manager"}}, + Secrets: []models.DockerSecret{{ID: "sec-1", Name: "tls-cert"}}, + Configs: []models.DockerConfig{{ID: "cfg-1", Name: "redis-config"}}, + }, + } +} + +// DockerHostView.Services returns a fresh slice; nil-guards return nil. +func TestHostViewsBranchcov0719late_DockerServices(t *testing.T) { + // (a) nil receiver. + var zero DockerHostView + if got := zero.Services(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Docker. + r := &Resource{ID: "dh-svc-1", Type: ResourceTypeAgent} + if got := NewDockerHostView(r).Services(); got != nil { + t.Fatalf("nil Docker: expected nil, got %+v", got) + } + // (a) Docker present, Services nil: append of nil yields nil. + r.Docker = &DockerData{} + if got := NewDockerHostView(r).Services(); got != nil { + t.Fatalf("nil Services slice: expected nil, got %+v", got) + } + // (b) Populated: fresh slice with equal contents. + r = dockerHostFixture() + got := NewDockerHostView(r).Services() + if len(got) != 1 || got[0].ID != "svc-1" || got[0].Stack != "web" { + t.Fatalf("expected cloned services, got %+v", got) + } + // Element value-field mutation on clone must not affect source. + got[0].ID = "mutated" + got[0].Stack = "leaked" + if r.Docker.Services[0].ID != "svc-1" || r.Docker.Services[0].Stack != "web" { + t.Fatalf("mutation leaked to source: %+v", r.Docker.Services[0]) + } + // Appending to clone must not affect source. + got = append(got, models.DockerService{ID: "extra"}) + if len(r.Docker.Services) != 1 { + t.Fatalf("append leaked to source: source len=%d want 1", len(r.Docker.Services)) + } +} + +// DockerHostView.Tasks returns a fresh slice (not deep-cloned per source comment). +func TestHostViewsBranchcov0719late_DockerTasks(t *testing.T) { + // (a) nil receiver. + var zero DockerHostView + if got := zero.Tasks(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Docker. + r := &Resource{ID: "dh-task-1", Type: ResourceTypeAgent} + if got := NewDockerHostView(r).Tasks(); got != nil { + t.Fatalf("nil Docker: expected nil, got %+v", got) + } + // (a) Docker present, Tasks nil. + r.Docker = &DockerData{} + if got := NewDockerHostView(r).Tasks(); got != nil { + t.Fatalf("nil Tasks slice: expected nil, got %+v", got) + } + // (b) Populated: fresh slice with equal contents. + r = dockerHostFixture() + got := NewDockerHostView(r).Tasks() + if len(got) != 1 || got[0].ID != "task-1" || got[0].NodeID != "node-1" { + t.Fatalf("expected cloned tasks, got %+v", got) + } + got[0].ID = "mutated" + got[0].NodeID = "leaked" + if r.Docker.Tasks[0].ID != "task-1" || r.Docker.Tasks[0].NodeID != "node-1" { + t.Fatalf("mutation leaked to source: %+v", r.Docker.Tasks[0]) + } +} + +// DockerHostView.Nodes returns a fresh slice. +func TestHostViewsBranchcov0719late_DockerNodes(t *testing.T) { + // (a) nil receiver. + var zero DockerHostView + if got := zero.Nodes(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Docker. + r := &Resource{ID: "dh-node-1", Type: ResourceTypeAgent} + if got := NewDockerHostView(r).Nodes(); got != nil { + t.Fatalf("nil Docker: expected nil, got %+v", got) + } + // (a) Docker present, Nodes nil. + r.Docker = &DockerData{} + if got := NewDockerHostView(r).Nodes(); got != nil { + t.Fatalf("nil Nodes slice: expected nil, got %+v", got) + } + // (b) Populated: fresh slice with equal contents. + r = dockerHostFixture() + got := NewDockerHostView(r).Nodes() + if len(got) != 1 || got[0].ID != "node-1" || got[0].Role != "manager" { + t.Fatalf("expected cloned nodes, got %+v", got) + } + got[0].ID = "mutated" + got[0].Role = "leaked" + if r.Docker.Nodes[0].ID != "node-1" || r.Docker.Nodes[0].Role != "manager" { + t.Fatalf("mutation leaked to source: %+v", r.Docker.Nodes[0]) + } +} + +// DockerHostView.Secrets returns a fresh slice. +func TestHostViewsBranchcov0719late_DockerSecrets(t *testing.T) { + // (a) nil receiver. + var zero DockerHostView + if got := zero.Secrets(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Docker. + r := &Resource{ID: "dh-sec-1", Type: ResourceTypeAgent} + if got := NewDockerHostView(r).Secrets(); got != nil { + t.Fatalf("nil Docker: expected nil, got %+v", got) + } + // (a) Docker present, Secrets nil. + r.Docker = &DockerData{} + if got := NewDockerHostView(r).Secrets(); got != nil { + t.Fatalf("nil Secrets slice: expected nil, got %+v", got) + } + // (b) Populated: fresh slice with equal contents. + r = dockerHostFixture() + got := NewDockerHostView(r).Secrets() + if len(got) != 1 || got[0].ID != "sec-1" || got[0].Name != "tls-cert" { + t.Fatalf("expected cloned secrets, got %+v", got) + } + got[0].ID = "mutated" + got[0].Name = "leaked" + if r.Docker.Secrets[0].ID != "sec-1" || r.Docker.Secrets[0].Name != "tls-cert" { + t.Fatalf("mutation leaked to source: %+v", r.Docker.Secrets[0]) + } +} + +// DockerHostView.Configs returns a fresh slice. +func TestHostViewsBranchcov0719late_DockerConfigs(t *testing.T) { + // (a) nil receiver. + var zero DockerHostView + if got := zero.Configs(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Docker. + r := &Resource{ID: "dh-cfg-1", Type: ResourceTypeAgent} + if got := NewDockerHostView(r).Configs(); got != nil { + t.Fatalf("nil Docker: expected nil, got %+v", got) + } + // (a) Docker present, Configs nil. + r.Docker = &DockerData{} + if got := NewDockerHostView(r).Configs(); got != nil { + t.Fatalf("nil Configs slice: expected nil, got %+v", got) + } + // (b) Populated: fresh slice with equal contents. + r = dockerHostFixture() + got := NewDockerHostView(r).Configs() + if len(got) != 1 || got[0].ID != "cfg-1" || got[0].Name != "redis-config" { + t.Fatalf("expected cloned configs, got %+v", got) + } + got[0].ID = "mutated" + got[0].Name = "leaked" + if r.Docker.Configs[0].ID != "cfg-1" || r.Docker.Configs[0].Name != "redis-config" { + t.Fatalf("mutation leaked to source: %+v", r.Docker.Configs[0]) + } +} + +// DockerHostView.Hidden is a three-way short-circuit boolean. +func TestHostViewsBranchcov0719late_DockerHidden(t *testing.T) { + // (a) nil receiver. + var zero DockerHostView + if zero.Hidden() { + t.Fatal("nil receiver: expected false") + } + // (a) nil Docker. + r := &Resource{ID: "dh-hid-1", Type: ResourceTypeAgent} + if NewDockerHostView(r).Hidden() { + t.Fatal("nil Docker: expected false") + } + // (b) Docker present, Hidden=false. + r.Docker = &DockerData{Hidden: false} + if NewDockerHostView(r).Hidden() { + t.Fatal("Hidden=false: expected false") + } + // (b) Docker present, Hidden=true. + r.Docker.Hidden = true + if !NewDockerHostView(r).Hidden() { + t.Fatal("Hidden=true: expected true") + } +} + +// DockerHostView.Security guards nil receiver / nil Docker / nil Security and +// returns a shallow copy with an independent AuthorizationPlugins slice. +func TestHostViewsBranchcov0719late_DockerSecurity(t *testing.T) { + // (a) nil receiver. + var zero DockerHostView + if got := zero.Security(); got != nil { + t.Fatalf("nil receiver: expected nil, got %+v", got) + } + // (a) nil Docker. + r := &Resource{ID: "dh-sec-1", Type: ResourceTypeAgent} + if got := NewDockerHostView(r).Security(); got != nil { + t.Fatalf("nil Docker: expected nil, got %+v", got) + } + // (a) Docker present, Security nil. + r.Docker = &DockerData{} + if got := NewDockerHostView(r).Security(); got != nil { + t.Fatalf("nil Security: expected nil, got %+v", got) + } + // (b) Populated: value-equal clone with independent AuthorizationPlugins. + r = dockerHostFixture() + got := NewDockerHostView(r).Security() + if got == nil { + t.Fatal("populated: expected non-nil") + } + if !reflect.DeepEqual(*got, *r.Docker.Security) { + t.Fatalf("expected value-equal clone, got %+v want %+v", *got, *r.Docker.Security) + } + // Mutate scalar on clone; source unaffected. + got.MutatingCommandsBlocked = false + if r.Docker.Security.MutatingCommandsBlocked != true { + t.Fatalf("scalar mutation leaked to source: %+v", r.Docker.Security) + } + // Mutate AuthorizationPlugins element; source unaffected. + got.AuthorizationPlugins[0] = "leaked" + if r.Docker.Security.AuthorizationPlugins[0] != "authz1" { + t.Fatalf("AuthorizationPlugins mutation leaked: %+v", r.Docker.Security.AuthorizationPlugins) + } + // Append to AuthorizationPlugins; source length unchanged. + got.AuthorizationPlugins = append(got.AuthorizationPlugins, "authz3") + if len(r.Docker.Security.AuthorizationPlugins) != 2 { + t.Fatalf("AuthorizationPlugins append leaked: source len=%d want 2", len(r.Docker.Security.AuthorizationPlugins)) + } +} diff --git a/internal/unifiedresources/k8sdeploymentview_branchcov0719late_test.go b/internal/unifiedresources/k8sdeploymentview_branchcov0719late_test.go new file mode 100644 index 000000000..4fe24bd60 --- /dev/null +++ b/internal/unifiedresources/k8sdeploymentview_branchcov0719late_test.go @@ -0,0 +1,253 @@ +package unifiedresources + +import ( + "reflect" + "testing" + "time" +) + +// TestK8sDeploymentViewBranchcov0719late_NilReceiver exercises the nil-receiver +// arm of every K8sDeploymentView accessor. A zero-value view (no backing +// *Resource) must return zero values without panicking. +func TestK8sDeploymentViewBranchcov0719late_NilReceiver(t *testing.T) { + var v K8sDeploymentView + + if got := v.ID(); got != "" { + t.Fatalf("nil-receiver ID: expected %q, got %q", "", got) + } + if got := v.Status(); got != "" { + t.Fatalf("nil-receiver Status: expected %q, got %q", "", got) + } + if got := v.ClusterName(); got != "" { + t.Fatalf("nil-receiver ClusterName: expected %q, got %q", "", got) + } + if got := v.DeploymentUID(); got != "" { + t.Fatalf("nil-receiver DeploymentUID: expected %q, got %q", "", got) + } + if got := v.DesiredReplicas(); got != 0 { + t.Fatalf("nil-receiver DesiredReplicas: expected 0, got %d", got) + } + if got := v.UpdatedReplicas(); got != 0 { + t.Fatalf("nil-receiver UpdatedReplicas: expected 0, got %d", got) + } + if got := v.ReadyReplicas(); got != 0 { + t.Fatalf("nil-receiver ReadyReplicas: expected 0, got %d", got) + } + if got := v.AvailableReplicas(); got != 0 { + t.Fatalf("nil-receiver AvailableReplicas: expected 0, got %d", got) + } + if got := v.Labels(); got != nil { + t.Fatalf("nil-receiver Labels: expected nil, got %+v", got) + } + if got := v.Tags(); got != nil { + t.Fatalf("nil-receiver Tags: expected nil, got %+v", got) + } + if got := v.LastSeen(); !got.IsZero() { + t.Fatalf("nil-receiver LastSeen: expected zero time, got %v", got) + } + if got, want := v.String(), `K8sDeploymentView(, "")`; got != want { + t.Fatalf("nil-receiver String: expected %q, got %q", want, got) + } +} + +// TestK8sDeploymentViewBranchcov0719late_NilKubernetesNested exercises the +// nil-nested arm of accessors that dereference v.r.Kubernetes. With a populated +// *Resource but nil Kubernetes payload, Kubernetes-backed accessors must return +// their zero values, while the resource-level accessors (ID/Status/Tags/LastSeen) +// still project the underlying value. +func TestK8sDeploymentViewBranchcov0719late_NilKubernetesNested(t *testing.T) { + now := time.Date(2026, 7, 19, 9, 30, 0, 0, time.UTC) + r := &Resource{ + ID: "k8sdep-no-k8s", + Type: ResourceTypeK8sDeployment, + Name: "orphan-deploy", + Status: StatusOnline, + LastSeen: now, + Tags: []string{"k8s", "app:orphan"}, + // Kubernetes deliberately nil. + } + v := NewK8sDeploymentView(r) + + // Kubernetes-backed accessors -> zero values (nil-nested arm). + if got := v.ClusterName(); got != "" { + t.Fatalf("nil-nested ClusterName: expected %q, got %q", "", got) + } + if got := v.DeploymentUID(); got != "" { + t.Fatalf("nil-nested DeploymentUID: expected %q, got %q", "", got) + } + if got := v.DesiredReplicas(); got != 0 { + t.Fatalf("nil-nested DesiredReplicas: expected 0, got %d", got) + } + if got := v.UpdatedReplicas(); got != 0 { + t.Fatalf("nil-nested UpdatedReplicas: expected 0, got %d", got) + } + if got := v.ReadyReplicas(); got != 0 { + t.Fatalf("nil-nested ReadyReplicas: expected 0, got %d", got) + } + if got := v.AvailableReplicas(); got != 0 { + t.Fatalf("nil-nested AvailableReplicas: expected 0, got %d", got) + } + if got := v.Labels(); got != nil { + t.Fatalf("nil-nested Labels: expected nil, got %+v", got) + } + + // Resource-backed accessors still project the underlying value (only nil-r + // guard, which is non-nil here). + if got := v.ID(); got != "k8sdep-no-k8s" { + t.Fatalf("nil-nested ID: expected %q, got %q", "k8sdep-no-k8s", got) + } + if got := v.Status(); got != StatusOnline { + t.Fatalf("nil-nested Status: expected %q, got %q", StatusOnline, got) + } + assertStringSlice(t, v.Tags(), []string{"k8s", "app:orphan"}) + if got := v.LastSeen(); !got.Equal(now) { + t.Fatalf("nil-nested LastSeen: expected %v, got %v", now, got) + } +} + +// TestK8sDeploymentViewBranchcov0719late_Populated exercises the populated arm +// of every accessor with a fully-populated Kubernetes payload, including +// clone-independence for Labels and Tags (mutating the returned value must not +// affect the backing resource). +func TestK8sDeploymentViewBranchcov0719late_Populated(t *testing.T) { + now := time.Date(2026, 7, 19, 10, 0, 0, 0, time.UTC) + r := &Resource{ + ID: "k8sdep-1", + Type: ResourceTypeK8sDeployment, + Name: "frontend", + Status: StatusOnline, + LastSeen: now, + Tags: []string{"k8s", "app:frontend"}, + Kubernetes: &K8sData{ + ClusterName: "prod-k8s", + Namespace: "web", + DeploymentUID: "deploy-uid-123", + DesiredReplicas: 3, + UpdatedReplicas: 2, + ReadyReplicas: 2, + AvailableReplicas: 1, + Labels: map[string]string{"app": "nginx", "tier": "web"}, + }, + } + v := NewK8sDeploymentView(r) + + if got := v.ID(); got != "k8sdep-1" { + t.Fatalf("populated ID: expected %q, got %q", "k8sdep-1", got) + } + if got := v.Status(); got != StatusOnline { + t.Fatalf("populated Status: expected %q, got %q", StatusOnline, got) + } + if got := v.ClusterName(); got != "prod-k8s" { + t.Fatalf("populated ClusterName: expected %q, got %q", "prod-k8s", got) + } + if got := v.DeploymentUID(); got != "deploy-uid-123" { + t.Fatalf("populated DeploymentUID: expected %q, got %q", "deploy-uid-123", got) + } + if got := v.DesiredReplicas(); got != 3 { + t.Fatalf("populated DesiredReplicas: expected 3, got %d", got) + } + if got := v.UpdatedReplicas(); got != 2 { + t.Fatalf("populated UpdatedReplicas: expected 2, got %d", got) + } + if got := v.ReadyReplicas(); got != 2 { + t.Fatalf("populated ReadyReplicas: expected 2, got %d", got) + } + if got := v.AvailableReplicas(); got != 1 { + t.Fatalf("populated AvailableReplicas: expected 1, got %d", got) + } + + wantLabels := map[string]string{"app": "nginx", "tier": "web"} + if got := v.Labels(); !reflect.DeepEqual(got, wantLabels) { + t.Fatalf("populated Labels: expected %+v, got %+v", wantLabels, got) + } + // Clone independence: mutating the returned map must not affect the source. + labelsClone := v.Labels() + labelsClone["app"] = "mutated" + delete(labelsClone, "tier") + labelsClone["new"] = "value" + if got := v.Labels(); !reflect.DeepEqual(got, wantLabels) { + t.Fatalf("populated Labels independence broken: source mutated to %+v", got) + } + + assertStringSlice(t, v.Tags(), []string{"k8s", "app:frontend"}) + // Clone independence: mutating the returned slice must not affect the source. + tagsClone := v.Tags() + if len(tagsClone) > 0 { + tagsClone[0] = "mutated" + } + if got, want := v.Tags(), ([]string{"k8s", "app:frontend"}); !reflect.DeepEqual(got, want) { + t.Fatalf("populated Tags independence broken: source mutated to %+v", got) + } + + if got := v.LastSeen(); !got.Equal(now) { + t.Fatalf("populated LastSeen: expected %v, got %v", now, got) + } + if got, want := v.String(), `K8sDeploymentView(k8sdep-1, "frontend")`; got != want { + t.Fatalf("populated String: expected %q, got %q", want, got) + } +} + +// TestK8sClusterViewBranchcov0719late_SourceStatusAgentVersionInterval +// exercises the nil-receiver, nil-Kubernetes-nested, and populated arms of the +// three K8sClusterView accessors delegated to the prompt. +func TestK8sClusterViewBranchcov0719late_SourceStatusAgentVersionInterval(t *testing.T) { + t.Run("NilReceiver", func(t *testing.T) { + var v K8sClusterView + if got := v.SourceStatus(); got != "" { + t.Fatalf("nil-receiver SourceStatus: expected %q, got %q", "", got) + } + if got := v.AgentVersion(); got != "" { + t.Fatalf("nil-receiver AgentVersion: expected %q, got %q", "", got) + } + if got := v.IntervalSeconds(); got != 0 { + t.Fatalf("nil-receiver IntervalSeconds: expected 0, got %d", got) + } + }) + + t.Run("NilKubernetesNested", func(t *testing.T) { + r := &Resource{ + ID: "k8s-no-payload", + Type: ResourceTypeK8sCluster, + Name: "orphan-cluster", + Status: StatusOnline, + LastSeen: time.Date(2026, 7, 19, 10, 30, 0, 0, time.UTC), + // Kubernetes deliberately nil. + } + v := NewK8sClusterView(r) + if got := v.SourceStatus(); got != "" { + t.Fatalf("nil-nested SourceStatus: expected %q, got %q", "", got) + } + if got := v.AgentVersion(); got != "" { + t.Fatalf("nil-nested AgentVersion: expected %q, got %q", "", got) + } + if got := v.IntervalSeconds(); got != 0 { + t.Fatalf("nil-nested IntervalSeconds: expected 0, got %d", got) + } + }) + + t.Run("Populated", func(t *testing.T) { + r := &Resource{ + ID: "k8s-1", + Type: ResourceTypeK8sCluster, + Name: "prod-k8s", + Status: StatusOnline, + LastSeen: time.Date(2026, 7, 19, 11, 0, 0, 0, time.UTC), + Kubernetes: &K8sData{ + ClusterName: "prod-k8s", + SourceStatus: "online", + AgentVersion: "1.5.0", + IntervalSeconds: 30, + }, + } + v := NewK8sClusterView(r) + if got := v.SourceStatus(); got != "online" { + t.Fatalf("populated SourceStatus: expected %q, got %q", "online", got) + } + if got := v.AgentVersion(); got != "1.5.0" { + t.Fatalf("populated AgentVersion: expected %q, got %q", "1.5.0", got) + } + if got := v.IntervalSeconds(); got != 30 { + t.Fatalf("populated IntervalSeconds: expected 30, got %d", got) + } + }) +} diff --git a/internal/unifiedresources/k8snodeview_branchcov0719late_test.go b/internal/unifiedresources/k8snodeview_branchcov0719late_test.go new file mode 100644 index 000000000..24cd89af4 --- /dev/null +++ b/internal/unifiedresources/k8snodeview_branchcov0719late_test.go @@ -0,0 +1,430 @@ +package unifiedresources + +import ( + "testing" + "time" +) + +// This file is a mechanical branch-coverage test for the K8sNodeView accessors +// in views.go. It exercises the nil-receiver arm, the nil-nested-Kubernetes +// arm (where present), and the fully-populated arm of every listed method. +// It deliberately does not touch any source file or pre-existing test. + +// k8sNodeBranchcov0719lateResource builds a fully-populated K8sNode *Resource +// exercising every field surfaced by K8sNodeView's accessors. +func k8sNodeBranchcov0719lateResource(now time.Time, parentID string) *Resource { + return &Resource{ + ID: "k8snode-1", + Type: ResourceTypeK8sNode, + Name: "worker-1", + Status: StatusOnline, + LastSeen: now, + Tags: []string{"k8s", "role:worker"}, + ParentID: &parentID, + Kubernetes: &K8sData{ + ClusterName: "prod-cluster", + NodeUID: "uid-node-1", + NodeName: "worker-1", + Ready: true, + Unschedulable: true, + Roles: []string{"control-plane", "worker"}, + KubeletVersion: "v1.31.0", + ContainerRuntimeVersion: "containerd://1.7.20", + OSImage: "Ubuntu 24.04 LTS", + KernelVersion: "6.8.0-45-generic", + Architecture: "amd64", + CapacityCPU: 16, + CapacityMemoryBytes: 32 * 1024 * 1024 * 1024, + CapacityPods: 110, + AllocCPU: 15, + AllocMemoryBytes: 30 * 1024 * 1024 * 1024, + AllocPods: 100, + }, + Metrics: &ResourceMetrics{ + CPU: &MetricValue{Percent: 42.5}, + Memory: &MetricValue{Percent: 67.25}, + }, + } +} + +// TestK8sNodeViewBranchcov0719late_NilReceiver covers the v.r == nil arm of +// every accessor plus the String() formatter and the NewK8sNodeView ctor +// (called with a nil *Resource). +func TestK8sNodeViewBranchcov0719late_NilReceiver(t *testing.T) { + v := NewK8sNodeView(nil) + + if got := v.ID(); got != "" { + t.Fatalf("ID() nil-receiver: want %q, got %q", "", got) + } + if got := v.Name(); got != "" { + t.Fatalf("Name() nil-receiver: want %q, got %q", "", got) + } + if got := v.Status(); got != ResourceStatus("") { + t.Fatalf("Status() nil-receiver: want empty, got %q", got) + } + if got := v.ClusterName(); got != "" { + t.Fatalf("ClusterName() nil-receiver: want %q, got %q", "", got) + } + if got := v.NodeUID(); got != "" { + t.Fatalf("NodeUID() nil-receiver: want %q, got %q", "", got) + } + if got := v.NodeName(); got != "" { + t.Fatalf("NodeName() nil-receiver: want %q, got %q", "", got) + } + if v.Ready() { + t.Fatalf("Ready() nil-receiver: want false, got true") + } + if v.Unschedulable() { + t.Fatalf("Unschedulable() nil-receiver: want false, got true") + } + if got := v.Roles(); got != nil { + t.Fatalf("Roles() nil-receiver: want nil, got %v", got) + } + if got := v.KubeletVersion(); got != "" { + t.Fatalf("KubeletVersion() nil-receiver: want %q, got %q", "", got) + } + if got := v.ContainerRuntimeVersion(); got != "" { + t.Fatalf("ContainerRuntimeVersion() nil-receiver: want %q, got %q", "", got) + } + if got := v.OSImage(); got != "" { + t.Fatalf("OSImage() nil-receiver: want %q, got %q", "", got) + } + if got := v.KernelVersion(); got != "" { + t.Fatalf("KernelVersion() nil-receiver: want %q, got %q", "", got) + } + if got := v.Architecture(); got != "" { + t.Fatalf("Architecture() nil-receiver: want %q, got %q", "", got) + } + if got := v.CapacityCPU(); got != 0 { + t.Fatalf("CapacityCPU() nil-receiver: want 0, got %d", got) + } + if got := v.CapacityMemoryBytes(); got != 0 { + t.Fatalf("CapacityMemoryBytes() nil-receiver: want 0, got %d", got) + } + if got := v.CapacityPods(); got != 0 { + t.Fatalf("CapacityPods() nil-receiver: want 0, got %d", got) + } + if got := v.AllocCPU(); got != 0 { + t.Fatalf("AllocCPU() nil-receiver: want 0, got %d", got) + } + if got := v.AllocMemoryBytes(); got != 0 { + t.Fatalf("AllocMemoryBytes() nil-receiver: want 0, got %d", got) + } + if got := v.AllocPods(); got != 0 { + t.Fatalf("AllocPods() nil-receiver: want 0, got %d", got) + } + if got := v.CPUPercent(); got != 0 { + t.Fatalf("CPUPercent() nil-receiver: want 0, got %v", got) + } + if got := v.MemoryPercent(); got != 0 { + t.Fatalf("MemoryPercent() nil-receiver: want 0, got %v", got) + } + if got := v.Tags(); got != nil { + t.Fatalf("Tags() nil-receiver: want nil, got %v", got) + } + if got := v.LastSeen(); !got.IsZero() { + t.Fatalf("LastSeen() nil-receiver: want zero time, got %v", got) + } + if got := v.ParentID(); got != "" { + t.Fatalf("ParentID() nil-receiver: want %q, got %q", "", got) + } + + // String() must not panic on a nil-receiver and must report empty id/name. + if got := v.String(); got != `K8sNodeView(, "")` { + t.Fatalf("String() nil-receiver: want %q, got %q", `K8sNodeView(, "")`, got) + } +} + +// TestK8sNodeViewBranchcov0719late_NilKubernetesNested covers the +// v.r.Kubernetes == nil arm of every Kubernetes-backed accessor. The +// non-Kubernetes accessors (ID/Name/Status/Tags/LastSeen/CPUPercent/ +// MemoryPercent/ParentID) are also exercised to confirm they still project +// from the outer Resource when Kubernetes is nil. +func TestK8sNodeViewBranchcov0719late_NilKubernetesNested(t *testing.T) { + now := time.Date(2026, 7, 19, 9, 30, 0, 0, time.UTC) + parentID := "cluster-parent-1" + r := &Resource{ + ID: "k8snode-no-k8s", + Type: ResourceTypeK8sNode, + Name: "worker-bare", + Status: StatusOffline, + LastSeen: now, + Tags: []string{"bare"}, + ParentID: &parentID, + // Kubernetes intentionally nil. + // Metrics intentionally nil so CPUPercent/MemoryPercent go through + // their nil-receiver-equivalent path via viewMetricPercent(nil, ...). + } + v := NewK8sNodeView(r) + + // Kubernetes-backed accessors: all should return their zero value. + if got := v.ClusterName(); got != "" { + t.Fatalf("ClusterName() Kubernetes==nil: want %q, got %q", "", got) + } + if got := v.NodeUID(); got != "" { + t.Fatalf("NodeUID() Kubernetes==nil: want %q, got %q", "", got) + } + if got := v.NodeName(); got != "" { + t.Fatalf("NodeName() Kubernetes==nil: want %q, got %q", "", got) + } + if v.Ready() { + t.Fatalf("Ready() Kubernetes==nil: want false, got true") + } + if v.Unschedulable() { + t.Fatalf("Unschedulable() Kubernetes==nil: want false, got true") + } + if got := v.Roles(); got != nil { + t.Fatalf("Roles() Kubernetes==nil: want nil, got %v", got) + } + if got := v.KubeletVersion(); got != "" { + t.Fatalf("KubeletVersion() Kubernetes==nil: want %q, got %q", "", got) + } + if got := v.ContainerRuntimeVersion(); got != "" { + t.Fatalf("ContainerRuntimeVersion() Kubernetes==nil: want %q, got %q", "", got) + } + if got := v.OSImage(); got != "" { + t.Fatalf("OSImage() Kubernetes==nil: want %q, got %q", "", got) + } + if got := v.KernelVersion(); got != "" { + t.Fatalf("KernelVersion() Kubernetes==nil: want %q, got %q", "", got) + } + if got := v.Architecture(); got != "" { + t.Fatalf("Architecture() Kubernetes==nil: want %q, got %q", "", got) + } + if got := v.CapacityCPU(); got != 0 { + t.Fatalf("CapacityCPU() Kubernetes==nil: want 0, got %d", got) + } + if got := v.CapacityMemoryBytes(); got != 0 { + t.Fatalf("CapacityMemoryBytes() Kubernetes==nil: want 0, got %d", got) + } + if got := v.CapacityPods(); got != 0 { + t.Fatalf("CapacityPods() Kubernetes==nil: want 0, got %d", got) + } + if got := v.AllocCPU(); got != 0 { + t.Fatalf("AllocCPU() Kubernetes==nil: want 0, got %d", got) + } + if got := v.AllocMemoryBytes(); got != 0 { + t.Fatalf("AllocMemoryBytes() Kubernetes==nil: want 0, got %d", got) + } + if got := v.AllocPods(); got != 0 { + t.Fatalf("AllocPods() Kubernetes==nil: want 0, got %d", got) + } + + // Non-Kubernetes accessors must still project the outer Resource values. + if got := v.ID(); got != "k8snode-no-k8s" { + t.Fatalf("ID() Kubernetes==nil: want %q, got %q", "k8snode-no-k8s", got) + } + if got := v.Name(); got != "worker-bare" { + t.Fatalf("Name() Kubernetes==nil: want %q, got %q", "worker-bare", got) + } + if got := v.Status(); got != StatusOffline { + t.Fatalf("Status() Kubernetes==nil: want %q, got %q", StatusOffline, got) + } + if got := v.CPUPercent(); got != 0 { + t.Fatalf("CPUPercent() Metrics==nil: want 0, got %v", got) + } + if got := v.MemoryPercent(); got != 0 { + t.Fatalf("MemoryPercent() Metrics==nil: want 0, got %v", got) + } + assertStringSlice(t, v.Tags(), []string{"bare"}) + if got := v.LastSeen(); !got.Equal(now) { + t.Fatalf("LastSeen() Kubernetes==nil: want %v, got %v", now, got) + } + if got := v.ParentID(); got != parentID { + t.Fatalf("ParentID() Kubernetes==nil: want %q, got %q", parentID, got) + } +} + +// TestK8sNodeViewBranchcov0719late_Populated covers the fully-populated arm of +// every accessor and verifies that slice-returning accessors (Roles, Tags) +// produce independent copies (defensive clone contract). +func TestK8sNodeViewBranchcov0719late_Populated(t *testing.T) { + now := time.Date(2026, 7, 19, 10, 0, 0, 0, time.UTC) + parentID := "cluster-parent-1" + r := k8sNodeBranchcov0719lateResource(now, parentID) + v := NewK8sNodeView(r) + + if got := v.ID(); got != "k8snode-1" { + t.Fatalf("ID(): want %q, got %q", "k8snode-1", got) + } + if got := v.Name(); got != "worker-1" { + t.Fatalf("Name(): want %q, got %q", "worker-1", got) + } + if got := v.Status(); got != StatusOnline { + t.Fatalf("Status(): want %q, got %q", StatusOnline, got) + } + if got := v.ClusterName(); got != "prod-cluster" { + t.Fatalf("ClusterName(): want %q, got %q", "prod-cluster", got) + } + if got := v.NodeUID(); got != "uid-node-1" { + t.Fatalf("NodeUID(): want %q, got %q", "uid-node-1", got) + } + if got := v.NodeName(); got != "worker-1" { + t.Fatalf("NodeName(): want %q, got %q", "worker-1", got) + } + if !v.Ready() { + t.Fatalf("Ready(): want true, got false") + } + if !v.Unschedulable() { + t.Fatalf("Unschedulable(): want true, got false") + } + assertStringSlice(t, v.Roles(), []string{"control-plane", "worker"}) + if got := v.KubeletVersion(); got != "v1.31.0" { + t.Fatalf("KubeletVersion(): want %q, got %q", "v1.31.0", got) + } + if got := v.ContainerRuntimeVersion(); got != "containerd://1.7.20" { + t.Fatalf("ContainerRuntimeVersion(): want %q, got %q", "containerd://1.7.20", got) + } + if got := v.OSImage(); got != "Ubuntu 24.04 LTS" { + t.Fatalf("OSImage(): want %q, got %q", "Ubuntu 24.04 LTS", got) + } + if got := v.KernelVersion(); got != "6.8.0-45-generic" { + t.Fatalf("KernelVersion(): want %q, got %q", "6.8.0-45-generic", got) + } + if got := v.Architecture(); got != "amd64" { + t.Fatalf("Architecture(): want %q, got %q", "amd64", got) + } + if got := v.CapacityCPU(); got != 16 { + t.Fatalf("CapacityCPU(): want 16, got %d", got) + } + const capMem = int64(32) * 1024 * 1024 * 1024 + if got := v.CapacityMemoryBytes(); got != capMem { + t.Fatalf("CapacityMemoryBytes(): want %d, got %d", capMem, got) + } + if got := v.CapacityPods(); got != 110 { + t.Fatalf("CapacityPods(): want 110, got %d", got) + } + if got := v.AllocCPU(); got != 15 { + t.Fatalf("AllocCPU(): want 15, got %d", got) + } + const allocMem = int64(30) * 1024 * 1024 * 1024 + if got := v.AllocMemoryBytes(); got != allocMem { + t.Fatalf("AllocMemoryBytes(): want %d, got %d", allocMem, got) + } + if got := v.AllocPods(); got != 100 { + t.Fatalf("AllocPods(): want 100, got %d", got) + } + if got := v.CPUPercent(); got != 42.5 { + t.Fatalf("CPUPercent(): want %v, got %v", 42.5, got) + } + if got := v.MemoryPercent(); got != 67.25 { + t.Fatalf("MemoryPercent(): want %v, got %v", 67.25, got) + } + assertStringSlice(t, v.Tags(), []string{"k8s", "role:worker"}) + if got := v.LastSeen(); !got.Equal(now) { + t.Fatalf("LastSeen(): want %v, got %v", now, got) + } + if got := v.ParentID(); got != parentID { + t.Fatalf("ParentID(): want %q, got %q", parentID, got) + } + + // Defensive-clone contract: mutating returned slices must not leak back + // into the backing *Resource. + roles := v.Roles() + if len(roles) != 2 { + t.Fatalf("Roles(): want len 2, got %d", len(roles)) + } + roles[0] = "MUTATED" + roles = append(roles, "extra") + if got := v.Roles(); len(got) != 2 || got[0] != "control-plane" || got[2-1] != "worker" { + t.Fatalf("Roles() independence: want [control-plane worker], got %v", got) + } + if r.Kubernetes.Roles[0] != "control-plane" { + t.Fatalf("Roles() mutation leaked into backing Resource: got %v", r.Kubernetes.Roles) + } + + tags := v.Tags() + if len(tags) != 2 { + t.Fatalf("Tags(): want len 2, got %d", len(tags)) + } + tags[0] = "MUTATED" + tags = append(tags, "extra") + if got := v.Tags(); len(got) != 2 || got[0] != "k8s" || got[1] != "role:worker" { + t.Fatalf("Tags() independence: want [k8s role:worker], got %v", got) + } + if r.Tags[0] != "k8s" { + t.Fatalf("Tags() mutation leaked into backing Resource: got %v", r.Tags) + } +} + +// TestK8sNodeViewBranchcov0719late_NilParentID covers the v.r.ParentID == nil +// arm of ParentID() specifically (r is non-nil, Kubernetes is populated, but +// ParentID is nil). +func TestK8sNodeViewBranchcov0719late_NilParentID(t *testing.T) { + now := time.Date(2026, 7, 19, 11, 0, 0, 0, time.UTC) + r := &Resource{ + ID: "k8snode-no-parent", + Type: ResourceTypeK8sNode, + Name: "orphan-node", + Status: StatusOnline, + LastSeen: now, + Kubernetes: &K8sData{ + NodeName: "orphan-node", + Ready: true, + }, + } + v := NewK8sNodeView(r) + if got := v.ParentID(); got != "" { + t.Fatalf("ParentID() ParentID==nil: want %q, got %q", "", got) + } + if got := v.NodeName(); got != "orphan-node" { + t.Fatalf("NodeName() populated arm: want %q, got %q", "orphan-node", got) + } +} + +// TestK8sNodeViewBranchcov0719late_StringAndCtor covers the String() formatter +// and the NewK8sNodeView constructor wrapping behavior on a populated +// resource. +func TestK8sNodeViewBranchcov0719late_StringAndCtor(t *testing.T) { + now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC) + parentID := "cluster-parent-1" + r := k8sNodeBranchcov0719lateResource(now, parentID) + v := NewK8sNodeView(r) + + want := `K8sNodeView(k8snode-1, "worker-1")` + if got := v.String(); got != want { + t.Fatalf("String(): want %q, got %q", want, got) + } + + // NewK8sNodeView must wrap the provided *Resource (ID() round-trips the + // same value the Resource carries). + if v.ID() != r.ID { + t.Fatalf("NewK8sNodeView wrap: want ID %q, got %q", r.ID, v.ID()) + } +} + +// TestK8sNodeViewBranchcov0719late_EmptyRolesAndTags covers the edge case +// where Kubernetes is non-nil but Roles is empty/nil, and Tags is empty/nil. +// cloneStringSlice(nil) returns nil; cloneStringSlice([]string{}) returns an +// empty (non-nil) slice — both must be handled without panic. +func TestK8sNodeViewBranchcov0719late_EmptyRolesAndTags(t *testing.T) { + r := &Resource{ + ID: "k8snode-empty-slices", + Type: ResourceTypeK8sNode, + Name: "empty-node", + Status: StatusOnline, + LastSeen: time.Date(2026, 7, 19, 13, 0, 0, 0, time.UTC), + Kubernetes: &K8sData{ + NodeName: "empty-node", + // Roles intentionally nil. + }, + // Tags intentionally nil. + } + v := NewK8sNodeView(r) + if got := v.Roles(); got != nil { + t.Fatalf("Roles() nil-slice: want nil, got %v", got) + } + if got := v.Tags(); got != nil { + t.Fatalf("Tags() nil-slice: want nil, got %v", got) + } + + // And the empty (non-nil) slice variant for both. + r.Kubernetes.Roles = []string{} + r.Tags = []string{} + if got := v.Roles(); len(got) != 0 { + t.Fatalf("Roles() empty-slice: want len 0, got %d (%v)", len(got), got) + } + if got := v.Tags(); len(got) != 0 { + t.Fatalf("Tags() empty-slice: want len 0, got %d (%v)", len(got), got) + } +} diff --git a/internal/unifiedresources/miscviews_branchcov0719late_test.go b/internal/unifiedresources/miscviews_branchcov0719late_test.go new file mode 100644 index 000000000..c7f5d95cd --- /dev/null +++ b/internal/unifiedresources/miscviews_branchcov0719late_test.go @@ -0,0 +1,204 @@ +package unifiedresources + +import ( + "reflect" + "testing" +) + +// These tests augment views_test.go with branch-coverage assertions for the +// misc-view accessors and String() formatters requested for the 0719late +// coverage pass. They reuse the package's existing testResource helper and +// table-driven style, and they intentionally never mutate source under test. + +func TestContainerViewPool_MiscViewsBranchcov0719late(t *testing.T) { + // Arm: nil receiver must return "" (v.r == nil branch). + var zero ContainerView + if got := zero.Pool(); got != "" { + t.Fatalf("nil receiver Pool: expected %q, got %q", "", got) + } + + // Arm: resource present but Proxmox payload nil must return "" (v.r.Proxmox == nil branch). + rNoProx := testResource(ResourceTypeSystemContainer) + rNoProx.Proxmox = nil + if got := NewContainerView(rNoProx).Pool(); got != "" { + t.Fatalf("nil Proxmox Pool: expected %q, got %q", "", got) + } + + // Arm: populated exercises strings.TrimSpace(" production "). + rPop := testResource(ResourceTypeSystemContainer) + rPop.Proxmox = &ProxmoxData{Pool: " production "} + if got, want := NewContainerView(rPop).Pool(), "production"; got != want { + t.Fatalf("populated Pool: expected %q, got %q", want, got) + } +} + +func TestNodeViewIsClusterMember_MiscViewsBranchcov0719late(t *testing.T) { + // Arm: nil receiver must return false (v.r == nil branch). + var zero NodeView + if zero.IsClusterMember() { + t.Fatalf("nil receiver IsClusterMember: expected %v, got %v", false, true) + } + + // Arm: resource present but Proxmox nil must return false (v.r.Proxmox == nil branch). + rNoProx := testResource(ResourceTypeAgent) + rNoProx.Proxmox = nil + if NewNodeView(rNoProx).IsClusterMember() { + t.Fatalf("nil Proxmox IsClusterMember: expected %v, got %v", false, true) + } + + // Arm: populated true projects the underlying flag. + rTrue := testResource(ResourceTypeAgent) + rTrue.Proxmox = &ProxmoxData{IsClusterMember: true} + if !NewNodeView(rTrue).IsClusterMember() { + t.Fatalf("populated IsClusterMember=true: expected %v, got %v", true, false) + } + + // Arm: populated false projects the underlying flag (explicit boundary). + rFalse := testResource(ResourceTypeAgent) + rFalse.Proxmox = &ProxmoxData{IsClusterMember: false} + if NewNodeView(rFalse).IsClusterMember() { + t.Fatalf("populated IsClusterMember=false: expected %v, got %v", false, true) + } +} + +func TestPhysicalDiskViewMetricResourceID_MiscViewsBranchcov0719late(t *testing.T) { + // Arm: nil receiver must return "" (v.r == nil branch). + var zero PhysicalDiskView + if got := zero.MetricResourceID(); got != "" { + t.Fatalf("nil receiver MetricResourceID: expected %q, got %q", "", got) + } + + // Arm: MetricsTarget non-nil wins, even when PhysicalDisk would otherwise resolve. + rTarget := testResource(ResourceTypePhysicalDisk) + rTarget.MetricsTarget = &MetricsTarget{ResourceID: "vc-1:disk:serial-abc"} + rTarget.PhysicalDisk = &PhysicalDiskMeta{Serial: "should-not-win"} + if got, want := NewPhysicalDiskView(rTarget).MetricResourceID(), "vc-1:disk:serial-abc"; got != want { + t.Fatalf("MetricsTarget arm: expected %q, got %q", want, got) + } + + // Arm: MetricsTarget nil and PhysicalDisk has Serial → PhysicalDiskMetaMetricID returns trimmed serial. + rSerial := testResource(ResourceTypePhysicalDisk) + rSerial.PhysicalDisk = &PhysicalDiskMeta{Serial: " SER123 "} + if got, want := NewPhysicalDiskView(rSerial).MetricResourceID(), "SER123"; got != want { + t.Fatalf("PhysicalDiskMetaMetricID serial fallback arm: expected %q, got %q", want, got) + } + + // Arm: MetricsTarget nil and PhysicalDisk nil → fallback to trimmed resource ID. + rFallback := testResource(ResourceTypePhysicalDisk) + rFallback.ID = " disk-id-1 " + rFallback.PhysicalDisk = nil + if got, want := NewPhysicalDiskView(rFallback).MetricResourceID(), "disk-id-1"; got != want { + t.Fatalf("PhysicalDiskMetaMetricID nil-disk fallback arm: expected %q, got %q", want, got) + } +} + +func TestPBSInstanceViewDatastores_MiscViewsBranchcov0719late(t *testing.T) { + // Arm: nil receiver must return nil (v.r == nil branch). + var zero PBSInstanceView + if got := zero.Datastores(); got != nil { + t.Fatalf("nil receiver Datastores: expected nil, got %+v", got) + } + + // Arm: resource present but PBS nil must return nil (v.r.PBS == nil branch). + rNoPBS := testResource(ResourceTypePBS) + rNoPBS.PBS = nil + if got := NewPBSInstanceView(rNoPBS).Datastores(); got != nil { + t.Fatalf("nil PBS Datastores: expected nil, got %+v", got) + } + + // Arm: PBS present but Datastores slice itself nil must still return nil. + rNilSlice := testResource(ResourceTypePBS) + rNilSlice.PBS = &PBSData{} + if got := NewPBSInstanceView(rNilSlice).Datastores(); got != nil { + t.Fatalf("nil Datastores slice: expected nil, got %+v", got) + } + + // Arm: populated multiple datastores project correctly AND the clone is independent. + want := []PBSDatastoreMeta{ + {Name: "fast", Total: 100, Used: 96, Available: 4, UsagePercent: 96, Status: "online", DeduplicationFactor: 1.6}, + {Name: "archive", Total: 1000, Used: 500, Available: 500, UsagePercent: 50, Status: "online"}, + } + rPop := testResource(ResourceTypePBS) + rPop.PBS = &PBSData{Datastores: want} + + got := NewPBSInstanceView(rPop).Datastores() + if !reflect.DeepEqual(got, want) { + t.Fatalf("populated Datastores: expected %+v, got %+v", want, got) + } + + // Mutating the returned slice (and its elements) must not affect the backing resource. + got[0].Name = "mutated" + got[0].Total = 9999 + again := NewPBSInstanceView(rPop).Datastores() + if !reflect.DeepEqual(again, want) { + t.Fatalf("Datastores independence: expected clone to remain %+v, got %+v", want, again) + } +} + +func TestPMGInstanceViewInstanceID_MiscViewsBranchcov0719late(t *testing.T) { + // Arm: nil receiver must return "" (v.r == nil branch). + var zero PMGInstanceView + if got := zero.InstanceID(); got != "" { + t.Fatalf("nil receiver InstanceID: expected %q, got %q", "", got) + } + + // Arm: resource present but PMG nil must return "" (v.r.PMG == nil branch). + rNoPMG := testResource(ResourceTypePMG) + rNoPMG.PMG = nil + if got := NewPMGInstanceView(rNoPMG).InstanceID(); got != "" { + t.Fatalf("nil PMG InstanceID: expected %q, got %q", "", got) + } + + // Arm: populated projects the underlying InstanceID verbatim. + rPop := testResource(ResourceTypePMG) + rPop.PMG = &PMGData{InstanceID: "pmg-instance-1"} + if got, want := NewPMGInstanceView(rPop).InstanceID(), "pmg-instance-1"; got != want { + t.Fatalf("populated InstanceID: expected %q, got %q", want, got) + } +} + +func TestViewsStringMethods_MiscViewsBranchcov0719late(t *testing.T) { + // Populated arm: each formatter must compose ID()/Name() as `(, "")`. + populatedCases := []struct { + label string + got string + want string + }{ + {"VMView", NewVMView(&Resource{ID: "vm-1", Type: ResourceTypeVM, Name: "app-vm"}).String(), `VMView(vm-1, "app-vm")`}, + {"ContainerView", NewContainerView(&Resource{ID: "ct-1", Type: ResourceTypeSystemContainer, Name: "db-ct"}).String(), `ContainerView(ct-1, "db-ct")`}, + {"NodeView", NewNodeView(&Resource{ID: "node-1", Type: ResourceTypeAgent, Name: "pve-node-1"}).String(), `NodeView(node-1, "pve-node-1")`}, + {"StoragePoolView", NewStoragePoolView(&Resource{ID: "storage-1", Type: ResourceTypeStorage, Name: "local-zfs"}).String(), `StoragePoolView(storage-1, "local-zfs")`}, + {"PhysicalDiskView", NewPhysicalDiskView(&Resource{ID: "disk-1", Type: ResourceTypePhysicalDisk, Name: "Samsung 990 PRO"}).String(), `PhysicalDiskView(disk-1, "Samsung 990 PRO")`}, + {"PBSInstanceView", NewPBSInstanceView(&Resource{ID: "pbs-1", Type: ResourceTypePBS, Name: "pbs-a"}).String(), `PBSInstanceView(pbs-1, "pbs-a")`}, + {"PMGInstanceView", NewPMGInstanceView(&Resource{ID: "pmg-1", Type: ResourceTypePMG, Name: "pmg-a"}).String(), `PMGInstanceView(pmg-1, "pmg-a")`}, + {"WorkloadView", NewWorkloadView(&Resource{ID: "vm-2", Type: ResourceTypeVM, Name: "web-vm"}).String(), `WorkloadView(vm-2, "web-vm")`}, + {"InfrastructureView", NewInfrastructureView(&Resource{ID: "host-1", Type: ResourceTypeAgent, Name: "agent-host-1"}).String(), `InfrastructureView(host-1, "agent-host-1")`}, + } + for _, c := range populatedCases { + if c.got != c.want { + t.Errorf("%s populated String(): expected %q, got %q", c.label, c.want, c.got) + } + } + + // Nil-receiver arm: every String() must not panic and must render the empty-id/empty-name form. + nilCases := []struct { + label string + got string + want string + }{ + {"VMView nil", VMView{}.String(), `VMView(, "")`}, + {"ContainerView nil", ContainerView{}.String(), `ContainerView(, "")`}, + {"NodeView nil", NodeView{}.String(), `NodeView(, "")`}, + {"StoragePoolView nil", StoragePoolView{}.String(), `StoragePoolView(, "")`}, + {"PhysicalDiskView nil", PhysicalDiskView{}.String(), `PhysicalDiskView(, "")`}, + {"PBSInstanceView nil", PBSInstanceView{}.String(), `PBSInstanceView(, "")`}, + {"PMGInstanceView nil", PMGInstanceView{}.String(), `PMGInstanceView(, "")`}, + {"WorkloadView nil", WorkloadView{}.String(), `WorkloadView(, "")`}, + {"InfrastructureView nil", InfrastructureView{}.String(), `InfrastructureView(, "")`}, + } + for _, c := range nilCases { + if c.got != c.want { + t.Errorf("%s String(): expected %q, got %q", c.label, c.want, c.got) + } + } +} diff --git a/internal/unifiedresources/podview_branchcov0719late_test.go b/internal/unifiedresources/podview_branchcov0719late_test.go new file mode 100644 index 000000000..0fbc1251c --- /dev/null +++ b/internal/unifiedresources/podview_branchcov0719late_test.go @@ -0,0 +1,388 @@ +package unifiedresources + +import ( + "testing" + "time" +) + +// branchcov0719latePodResource builds a fully-populated Pod resource used to +// exercise the populated arm of every PodView accessor. The nil-receiver and +// nil-Kubernetes arms are covered separately in each test below. +func branchcov0719latePodResource(now time.Time) *Resource { + return &Resource{ + ID: "pod-id-1", + Type: ResourceTypePod, + Name: "pod-name-1", + Status: StatusOnline, + LastSeen: now, + Tags: []string{"app:web", "tier:frontend"}, + Kubernetes: &K8sData{ + ClusterID: "cluster-id-1", + ClusterName: "prod-cluster", + NodeName: "node-a", + Namespace: "checkout", + PodUID: "uid-1234", + PodPhase: "Running", + PodReason: "OutOfcpu", + PodMessage: "Pod was terminated due to cpu limit.", + Restarts: 3, + OwnerKind: "ReplicaSet", + OwnerName: "checkout-abc", + Image: "ghcr.io/example/checkout:v1.2.3", + PodContainers: []K8sPodContainer{ + {Name: "app", Image: "ghcr.io/example/checkout:v1.2.3", Ready: true, RestartCount: 1, State: "running"}, + {Name: "sidecar", Image: "busybox:1.36", Ready: false, RestartCount: 2, State: "waiting", Reason: "CrashLoopBackOff", Message: "Back-off pulling image"}, + }, + }, + Metrics: &ResourceMetrics{ + CPU: &MetricValue{Percent: 42.5}, + Memory: &MetricValue{Percent: 55.0}, + Disk: &MetricValue{Percent: 12.0}, + NetIn: &MetricValue{Value: 1024.5}, + NetOut: &MetricValue{Value: 2048.0}, + }, + } +} + +// TestPodViewBranchcov0719late_StringIDStatus covers String, ID and Status +// across the nil-receiver and populated arms. +func TestPodViewBranchcov0719late_StringIDStatus(t *testing.T) { + now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC) + + // Nil-receiver arm: every accessor must return its zero value without panic. + t.Run("NilReceiver", func(t *testing.T) { + var zero PodView + if got := zero.String(); got != `PodView(, "")` { + t.Fatalf("nil String(): got %q want %q", got, `PodView(, "")`) + } + if zero.ID() != "" { + t.Fatalf("nil ID(): got %q want empty", zero.ID()) + } + if zero.Status() != ResourceStatus("") { + t.Fatalf("nil Status(): got %q want empty", zero.Status()) + } + }) + + // Populated arm. + t.Run("Populated", func(t *testing.T) { + v := NewPodView(branchcov0719latePodResource(now)) + if got, want := v.String(), `PodView(pod-id-1, "pod-name-1")`; got != want { + t.Fatalf("String(): got %q want %q", got, want) + } + if v.ID() != "pod-id-1" { + t.Fatalf("ID(): got %q want %q", v.ID(), "pod-id-1") + } + if v.Status() != StatusOnline { + t.Fatalf("Status(): got %q want %q", v.Status(), StatusOnline) + } + }) +} + +// TestPodViewBranchcov0719late_KubernetesFields covers the Kubernetes-backed +// accessors: ClusterName, ClusterID, NodeName, PodUID, PodPhase, Restarts, +// OwnerKind, OwnerName, Image, PodReason, PodMessage. Each has both a +// nil-receiver arm and a nil-Kubernetes-nested arm, plus the populated arm. +func TestPodViewBranchcov0719late_KubernetesFields(t *testing.T) { + now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC) + + t.Run("NilReceiver", func(t *testing.T) { + var zero PodView + if zero.ClusterName() != "" { + t.Fatalf("nil ClusterName(): got %q want empty", zero.ClusterName()) + } + if zero.ClusterID() != "" { + t.Fatalf("nil ClusterID(): got %q want empty", zero.ClusterID()) + } + if zero.NodeName() != "" { + t.Fatalf("nil NodeName(): got %q want empty", zero.NodeName()) + } + if zero.PodUID() != "" { + t.Fatalf("nil PodUID(): got %q want empty", zero.PodUID()) + } + if zero.PodPhase() != "" { + t.Fatalf("nil PodPhase(): got %q want empty", zero.PodPhase()) + } + if zero.Restarts() != 0 { + t.Fatalf("nil Restarts(): got %d want 0", zero.Restarts()) + } + if zero.OwnerKind() != "" { + t.Fatalf("nil OwnerKind(): got %q want empty", zero.OwnerKind()) + } + if zero.OwnerName() != "" { + t.Fatalf("nil OwnerName(): got %q want empty", zero.OwnerName()) + } + if zero.Image() != "" { + t.Fatalf("nil Image(): got %q want empty", zero.Image()) + } + if zero.PodReason() != "" { + t.Fatalf("nil PodReason(): got %q want empty", zero.PodReason()) + } + if zero.PodMessage() != "" { + t.Fatalf("nil PodMessage(): got %q want empty", zero.PodMessage()) + } + }) + + // Nested-nil arm: Resource is present but Kubernetes is nil. + t.Run("NilKubernetes", func(t *testing.T) { + r := testResource(ResourceTypePod) + r.Kubernetes = nil + v := NewPodView(r) + + if v.ClusterName() != "" { + t.Fatalf("ClusterName() with nil Kubernetes: got %q want empty", v.ClusterName()) + } + if v.ClusterID() != "" { + t.Fatalf("ClusterID() with nil Kubernetes: got %q want empty", v.ClusterID()) + } + if v.NodeName() != "" { + t.Fatalf("NodeName() with nil Kubernetes: got %q want empty", v.NodeName()) + } + if v.PodUID() != "" { + t.Fatalf("PodUID() with nil Kubernetes: got %q want empty", v.PodUID()) + } + if v.PodPhase() != "" { + t.Fatalf("PodPhase() with nil Kubernetes: got %q want empty", v.PodPhase()) + } + if v.Restarts() != 0 { + t.Fatalf("Restarts() with nil Kubernetes: got %d want 0", v.Restarts()) + } + if v.OwnerKind() != "" { + t.Fatalf("OwnerKind() with nil Kubernetes: got %q want empty", v.OwnerKind()) + } + if v.OwnerName() != "" { + t.Fatalf("OwnerName() with nil Kubernetes: got %q want empty", v.OwnerName()) + } + if v.Image() != "" { + t.Fatalf("Image() with nil Kubernetes: got %q want empty", v.Image()) + } + if v.PodReason() != "" { + t.Fatalf("PodReason() with nil Kubernetes: got %q want empty", v.PodReason()) + } + if v.PodMessage() != "" { + t.Fatalf("PodMessage() with nil Kubernetes: got %q want empty", v.PodMessage()) + } + }) + + // Populated arm: each accessor projects its backing field verbatim. + t.Run("Populated", func(t *testing.T) { + v := NewPodView(branchcov0719latePodResource(now)) + if v.ClusterName() != "prod-cluster" { + t.Fatalf("ClusterName(): got %q want %q", v.ClusterName(), "prod-cluster") + } + if v.ClusterID() != "cluster-id-1" { + t.Fatalf("ClusterID(): got %q want %q", v.ClusterID(), "cluster-id-1") + } + if v.NodeName() != "node-a" { + t.Fatalf("NodeName(): got %q want %q", v.NodeName(), "node-a") + } + if v.PodUID() != "uid-1234" { + t.Fatalf("PodUID(): got %q want %q", v.PodUID(), "uid-1234") + } + if v.PodPhase() != "Running" { + t.Fatalf("PodPhase(): got %q want %q", v.PodPhase(), "Running") + } + if v.Restarts() != 3 { + t.Fatalf("Restarts(): got %d want 3", v.Restarts()) + } + if v.OwnerKind() != "ReplicaSet" { + t.Fatalf("OwnerKind(): got %q want %q", v.OwnerKind(), "ReplicaSet") + } + if v.OwnerName() != "checkout-abc" { + t.Fatalf("OwnerName(): got %q want %q", v.OwnerName(), "checkout-abc") + } + if v.Image() != "ghcr.io/example/checkout:v1.2.3" { + t.Fatalf("Image(): got %q want %q", v.Image(), "ghcr.io/example/checkout:v1.2.3") + } + if v.PodReason() != "OutOfcpu" { + t.Fatalf("PodReason(): got %q want %q", v.PodReason(), "OutOfcpu") + } + if v.PodMessage() != "Pod was terminated due to cpu limit." { + t.Fatalf("PodMessage(): got %q want %q", v.PodMessage(), "Pod was terminated due to cpu limit.") + } + }) +} + +// TestPodViewBranchcov0719late_Metrics covers CPUPercent, MemoryPercent, +// DiskPercent, NetInRate and NetOutRate. The defensive nil-receiver arm +// returns 0; the nil-Metrics nested arm also routes through +// viewMetricPercent/viewMetricValue and returns 0. +func TestPodViewBranchcov0719late_Metrics(t *testing.T) { + now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC) + + t.Run("NilReceiver", func(t *testing.T) { + var zero PodView + if zero.CPUPercent() != 0 { + t.Fatalf("nil CPUPercent(): got %v want 0", zero.CPUPercent()) + } + if zero.MemoryPercent() != 0 { + t.Fatalf("nil MemoryPercent(): got %v want 0", zero.MemoryPercent()) + } + if zero.DiskPercent() != 0 { + t.Fatalf("nil DiskPercent(): got %v want 0", zero.DiskPercent()) + } + if zero.NetInRate() != 0 { + t.Fatalf("nil NetInRate(): got %v want 0", zero.NetInRate()) + } + if zero.NetOutRate() != 0 { + t.Fatalf("nil NetOutRate(): got %v want 0", zero.NetOutRate()) + } + }) + + t.Run("NilMetrics", func(t *testing.T) { + r := testResource(ResourceTypePod) + r.Metrics = nil + v := NewPodView(r) + if v.CPUPercent() != 0 || v.MemoryPercent() != 0 || v.DiskPercent() != 0 || v.NetInRate() != 0 || v.NetOutRate() != 0 { + t.Fatalf("metric accessors with nil Metrics: got cpu=%v mem=%v disk=%v netIn=%v netOut=%v, all want 0", + v.CPUPercent(), v.MemoryPercent(), v.DiskPercent(), v.NetInRate(), v.NetOutRate()) + } + }) + + t.Run("NilNestedMetricValues", func(t *testing.T) { + // ResourceMetrics present but the individual MetricValue pointers nil. + r := testResource(ResourceTypePod) + r.Metrics = &ResourceMetrics{} + v := NewPodView(r) + if v.CPUPercent() != 0 || v.MemoryPercent() != 0 || v.DiskPercent() != 0 || v.NetInRate() != 0 || v.NetOutRate() != 0 { + t.Fatalf("metric accessors with nil nested MetricValue: got cpu=%v mem=%v disk=%v netIn=%v netOut=%v", + v.CPUPercent(), v.MemoryPercent(), v.DiskPercent(), v.NetInRate(), v.NetOutRate()) + } + }) + + t.Run("Populated", func(t *testing.T) { + v := NewPodView(branchcov0719latePodResource(now)) + if v.CPUPercent() != 42.5 { + t.Fatalf("CPUPercent(): got %v want 42.5", v.CPUPercent()) + } + if v.MemoryPercent() != 55.0 { + t.Fatalf("MemoryPercent(): got %v want 55", v.MemoryPercent()) + } + if v.DiskPercent() != 12.0 { + t.Fatalf("DiskPercent(): got %v want 12", v.DiskPercent()) + } + if v.NetInRate() != 1024.5 { + t.Fatalf("NetInRate(): got %v want 1024.5", v.NetInRate()) + } + if v.NetOutRate() != 2048.0 { + t.Fatalf("NetOutRate(): got %v want 2048", v.NetOutRate()) + } + }) +} + +// TestPodViewBranchcov0719late_Tags covers Tags: nil-receiver, nil field, and +// populated arms, plus clone independence. +func TestPodViewBranchcov0719late_Tags(t *testing.T) { + now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC) + + t.Run("NilReceiver", func(t *testing.T) { + var zero PodView + if got := zero.Tags(); got != nil { + t.Fatalf("nil Tags(): got %v want nil", got) + } + }) + + t.Run("NilField", func(t *testing.T) { + r := testResource(ResourceTypePod) + r.Tags = nil + v := NewPodView(r) + if got := v.Tags(); got != nil { + t.Fatalf("Tags() with nil Tags field: got %v want nil", got) + } + }) + + t.Run("Populated", func(t *testing.T) { + v := NewPodView(branchcov0719latePodResource(now)) + assertStringSlice(t, v.Tags(), []string{"app:web", "tier:frontend"}) + }) + + t.Run("CloneIsIndependent", func(t *testing.T) { + src := branchcov0719latePodResource(now) + v := NewPodView(src) + got := v.Tags() + got[0] = "mutated" + if src.Tags[0] != "app:web" { + t.Fatalf("Tags() did not return an independent copy; underlying slice mutated to %q", src.Tags[0]) + } + }) +} + +// TestPodViewBranchcov0719late_LastSeen covers LastSeen nil-receiver and +// populated arms. +func TestPodViewBranchcov0719late_LastSeen(t *testing.T) { + now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC) + + t.Run("NilReceiver", func(t *testing.T) { + var zero PodView + if !zero.LastSeen().IsZero() { + t.Fatalf("nil LastSeen(): got %v want zero time", zero.LastSeen()) + } + }) + + t.Run("Populated", func(t *testing.T) { + v := NewPodView(branchcov0719latePodResource(now)) + if !v.LastSeen().Equal(now) { + t.Fatalf("LastSeen(): got %v want %v", v.LastSeen(), now) + } + }) +} + +// TestPodViewBranchcov0719late_PodContainers covers PodContainers: nil +// receiver, nil Kubernetes facet, empty backing slice, populated, and clone +// independence. +func TestPodViewBranchcov0719late_PodContainers(t *testing.T) { + now := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC) + + t.Run("NilReceiver", func(t *testing.T) { + var zero PodView + if got := zero.PodContainers(); got != nil { + t.Fatalf("nil PodContainers(): got %v want nil", got) + } + }) + + t.Run("NilKubernetes", func(t *testing.T) { + r := testResource(ResourceTypePod) + r.Kubernetes = nil + v := NewPodView(r) + if got := v.PodContainers(); got != nil { + t.Fatalf("PodContainers() with nil Kubernetes: got %v want nil", got) + } + }) + + t.Run("EmptySliceReturnsNil", func(t *testing.T) { + // Backing slice is empty/nil — the len==0 guard must return nil. + r := testResource(ResourceTypePod) + r.Kubernetes = &K8sData{} + v := NewPodView(r) + if got := v.PodContainers(); got != nil { + t.Fatalf("PodContainers() with empty backing slice: got %v want nil", got) + } + }) + + t.Run("Populated", func(t *testing.T) { + v := NewPodView(branchcov0719latePodResource(now)) + got := v.PodContainers() + if len(got) != 2 { + t.Fatalf("PodContainers(): got %d items want 2: %+v", len(got), got) + } + if got[0].Name != "app" || got[0].Image != "ghcr.io/example/checkout:v1.2.3" || got[0].Ready != true || got[0].RestartCount != 1 || got[0].State != "running" { + t.Fatalf("PodContainers()[0] mismatch: got %+v", got[0]) + } + if got[1].Name != "sidecar" || got[1].Reason != "CrashLoopBackOff" || got[1].State != "waiting" || got[1].Message != "Back-off pulling image" { + t.Fatalf("PodContainers()[1] mismatch: got %+v", got[1]) + } + }) + + t.Run("CloneIsIndependent", func(t *testing.T) { + src := branchcov0719latePodResource(now) + v := NewPodView(src) + got := v.PodContainers() + got[0].Name = "mutated" + got[1].RestartCount = 99 + if src.Kubernetes.PodContainers[0].Name != "app" { + t.Fatalf("PodContainers() did not return an independent copy; element[0].Name mutated to %q", src.Kubernetes.PodContainers[0].Name) + } + if src.Kubernetes.PodContainers[1].RestartCount != 2 { + t.Fatalf("PodContainers() did not return an independent copy; element[1].RestartCount mutated to %d", src.Kubernetes.PodContainers[1].RestartCount) + } + }) +}