mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-10 02:25:56 +00:00
Add Go branch-coverage tests for pure helpers across six packages
Test-only wave, contract-neutral. New *_branchcov0719pm_test.go files cover previously-uncovered pure value-in/value-out helpers, each verified to move its target functions from 0% to full coverage: - internal/agentcontext: formatKubernetesServicePorts (empty/single/cap/overflow arms) and addMetricFact (nil-metric, percent/value/ratio arms) now 100%. - pkg/reporting: reportLogoTypeFromPath 0->100, reportLogoTypeFromData 28.6->100, scaledLogoSize 70->90 (extension and aspect branches). - internal/cloudcp/email: RenderMagicLinkEmail 0->80 (render success path; the compile-time template-error arm is unreachable and left uncovered). - internal/recovery: recoveryDetailString (nil map, missing key, non-string, string arms) and recoveryPointObservedAt 40->100. - internal/ai/tools: ErrStrictResolution/ErrRoutingMismatch ToToolResponse 100. - internal/ai/providers: every NormalizeCollections receiver 0->100. No source or existing test modified.
This commit is contained in:
@@ -0,0 +1,284 @@
|
||||
package agentcontext
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
unified "github.com/rcourtman/pulse-go-rewrite/internal/unifiedresources"
|
||||
)
|
||||
|
||||
// TestFormatKubernetesServicePortsBranchCov exercises every branch of
|
||||
// formatKubernetesServicePorts: the empty-input early return, the regular
|
||||
// formatting arm in the bounded loop, and the truncation ("N more") arm
|
||||
// triggered once the loop index reaches the internal cap of 5.
|
||||
func TestFormatKubernetesServicePortsBranchCov(t *testing.T) {
|
||||
five := []unified.K8sServicePort{
|
||||
{Port: 1, TargetPort: "a", Protocol: "TCP"},
|
||||
{Port: 2, TargetPort: "b", Protocol: "TCP"},
|
||||
{Port: 3, TargetPort: "c", Protocol: "TCP"},
|
||||
{Port: 4, TargetPort: "d", Protocol: "TCP"},
|
||||
{Port: 5, TargetPort: "e", Protocol: "TCP"},
|
||||
}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
ports []unified.K8sServicePort
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil slice returns empty string",
|
||||
ports: nil,
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "empty slice returns empty string",
|
||||
ports: []unified.K8sServicePort{},
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "single port formats as port:targetPort/protocol",
|
||||
ports: []unified.K8sServicePort{
|
||||
{Name: "http", Port: 80, TargetPort: "http", Protocol: "TCP", NodePort: 30080},
|
||||
},
|
||||
want: "80:http/TCP",
|
||||
},
|
||||
{
|
||||
name: "two ports joined with comma-space",
|
||||
ports: []unified.K8sServicePort{
|
||||
{Port: 80, TargetPort: "http", Protocol: "TCP"},
|
||||
{Port: 443, TargetPort: "https", Protocol: "TCP"},
|
||||
},
|
||||
want: "80:http/TCP, 443:https/TCP",
|
||||
},
|
||||
{
|
||||
name: "exactly five ports renders every entry without truncation",
|
||||
ports: five,
|
||||
want: "1:a/TCP, 2:b/TCP, 3:c/TCP, 4:d/TCP, 5:e/TCP",
|
||||
},
|
||||
{
|
||||
name: "six ports truncates to five plus one more suffix",
|
||||
ports: append(append([]unified.K8sServicePort{}, five...),
|
||||
unified.K8sServicePort{Port: 6, TargetPort: "f", Protocol: "TCP"},
|
||||
),
|
||||
want: "1:a/TCP, 2:b/TCP, 3:c/TCP, 4:d/TCP, 5:e/TCP, 1 more",
|
||||
},
|
||||
{
|
||||
name: "eight ports truncates to five plus three more suffix",
|
||||
ports: append(append([]unified.K8sServicePort{}, five...),
|
||||
unified.K8sServicePort{Port: 6, TargetPort: "f", Protocol: "TCP"},
|
||||
unified.K8sServicePort{Port: 7, TargetPort: "g", Protocol: "TCP"},
|
||||
unified.K8sServicePort{Port: 8, TargetPort: "h", Protocol: "TCP"},
|
||||
),
|
||||
want: "1:a/TCP, 2:b/TCP, 3:c/TCP, 4:d/TCP, 5:e/TCP, 3 more",
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := formatKubernetesServicePorts(tc.ports)
|
||||
if got != tc.want {
|
||||
t.Fatalf("formatKubernetesServicePorts(%+v) = %q, want %q", tc.ports, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestAddMetricFactBranchCov exercises every branch of addMetricFact and the
|
||||
// downstream empty-value skip in addAgentContextFact: the nil-metric early
|
||||
// return, each arm of the metric switch (Percent, Value, Used/Total), the
|
||||
// empty-value skip when no arm matches, and propagation of observedAt
|
||||
// through both its nil and non-nil forms.
|
||||
func TestAddMetricFactBranchCov(t *testing.T) {
|
||||
observedAt := time.Date(2026, 7, 19, 12, 0, 0, 0, time.UTC)
|
||||
|
||||
t.Run("nil_metric_does_not_append", func(t *testing.T) {
|
||||
facts := []Fact{{Label: "preexisting", Value: "v"}}
|
||||
before := make([]Fact, len(facts))
|
||||
copy(before, facts)
|
||||
|
||||
addMetricFact(&facts, "CPU", nil, &observedAt)
|
||||
|
||||
if len(facts) != len(before) {
|
||||
t.Fatalf("nil metric appended: before=%d after=%d (%+v)", len(before), len(facts), facts)
|
||||
}
|
||||
if facts[0].Label != "preexisting" || facts[0].Value != "v" {
|
||||
t.Fatalf("existing slice mutated by nil metric: %+v", facts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("nil_slice_unchanged_when_metric_nil", func(t *testing.T) {
|
||||
var facts []Fact
|
||||
addMetricFact(&facts, "CPU", nil, &observedAt)
|
||||
if facts != nil {
|
||||
t.Fatalf("nil slice promoted to non-nil by nil metric: %+v", facts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("percent_arm_appends_percentage_with_non_nil_observedAt", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
metric := &unified.MetricValue{Percent: 42.5}
|
||||
addMetricFact(&facts, "CPU", metric, &observedAt)
|
||||
|
||||
if len(facts) != 1 {
|
||||
t.Fatalf("expected exactly 1 fact, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
fact := facts[0]
|
||||
if fact.Label != "CPU" {
|
||||
t.Fatalf("Label = %q, want CPU", fact.Label)
|
||||
}
|
||||
if fact.Value != "42.5%" {
|
||||
t.Fatalf("Value = %q, want 42.5%%", fact.Value)
|
||||
}
|
||||
if fact.Source != agentContextSourceUnifiedResource {
|
||||
t.Fatalf("Source = %q, want %q", fact.Source, agentContextSourceUnifiedResource)
|
||||
}
|
||||
if fact.TrustTier != agentContextTrustRuntimeObserved {
|
||||
t.Fatalf("TrustTier = %q, want %q", fact.TrustTier, agentContextTrustRuntimeObserved)
|
||||
}
|
||||
if fact.ObservedAt == nil || !fact.ObservedAt.Equal(observedAt) {
|
||||
t.Fatalf("ObservedAt = %v, want %v", fact.ObservedAt, observedAt)
|
||||
}
|
||||
if fact.Redacted {
|
||||
t.Fatalf("Redacted = true, want false")
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("value_arm_appends_value_unit_with_nil_observedAt", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
metric := &unified.MetricValue{Value: 1.5, Unit: "GB"}
|
||||
addMetricFact(&facts, "Memory", metric, nil)
|
||||
|
||||
if len(facts) != 1 {
|
||||
t.Fatalf("expected exactly 1 fact, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
fact := facts[0]
|
||||
if fact.Label != "Memory" {
|
||||
t.Fatalf("Label = %q, want Memory", fact.Label)
|
||||
}
|
||||
if fact.Value != "1.5 GB" {
|
||||
t.Fatalf("Value = %q, want \"1.5 GB\"", fact.Value)
|
||||
}
|
||||
if fact.ObservedAt != nil {
|
||||
t.Fatalf("ObservedAt = %v, want nil when caller passes nil", fact.ObservedAt)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("value_arm_trims_surrounding_whitespace_in_unit", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
metric := &unified.MetricValue{Value: 4.0, Unit: " MiB "}
|
||||
addMetricFact(&facts, "Memory", metric, &observedAt)
|
||||
|
||||
if len(facts) != 1 {
|
||||
t.Fatalf("expected exactly 1 fact, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
if got := facts[0].Value; got != "4.0 MiB" {
|
||||
t.Fatalf("Value = %q, want \"4.0 MiB\" (unit trimmed)", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("used_total_arm_appends_ratio_when_total_positive", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
used := int64(100)
|
||||
total := int64(200)
|
||||
metric := &unified.MetricValue{Used: &used, Total: &total}
|
||||
addMetricFact(&facts, "Disk", metric, &observedAt)
|
||||
|
||||
if len(facts) != 1 {
|
||||
t.Fatalf("expected exactly 1 fact, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
if got := facts[0].Value; got != "100/200" {
|
||||
t.Fatalf("Value = %q, want \"100/200\"", got)
|
||||
}
|
||||
if got := facts[0].Label; got != "Disk" {
|
||||
t.Fatalf("Label = %q, want Disk", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("used_total_arm_skipped_when_total_zero", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
used := int64(50)
|
||||
total := int64(0)
|
||||
metric := &unified.MetricValue{Used: &used, Total: &total}
|
||||
addMetricFact(&facts, "Disk", metric, &observedAt)
|
||||
|
||||
if len(facts) != 0 {
|
||||
t.Fatalf("expected 0 facts when Total<=0, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("used_total_arm_skipped_when_total_negative", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
used := int64(50)
|
||||
total := int64(-1)
|
||||
metric := &unified.MetricValue{Used: &used, Total: &total}
|
||||
addMetricFact(&facts, "Disk", metric, &observedAt)
|
||||
|
||||
if len(facts) != 0 {
|
||||
t.Fatalf("expected 0 facts when Total<0, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("used_total_arm_skipped_when_used_nil", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
total := int64(100)
|
||||
metric := &unified.MetricValue{Total: &total}
|
||||
addMetricFact(&facts, "Disk", metric, &observedAt)
|
||||
|
||||
if len(facts) != 0 {
|
||||
t.Fatalf("expected 0 facts when Used is nil, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero_metric_produces_empty_value_and_skips_append", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
metric := &unified.MetricValue{}
|
||||
addMetricFact(&facts, "Network in", metric, &observedAt)
|
||||
|
||||
if len(facts) != 0 {
|
||||
t.Fatalf("expected 0 facts for zero metric, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("percent_arm_takes_precedence_over_value_and_used_total", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
used := int64(100)
|
||||
total := int64(200)
|
||||
metric := &unified.MetricValue{Percent: 75.0, Value: 9.9, Unit: "GB", Used: &used, Total: &total}
|
||||
addMetricFact(&facts, "CPU", metric, &observedAt)
|
||||
|
||||
if len(facts) != 1 {
|
||||
t.Fatalf("expected exactly 1 fact, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
if got := facts[0].Value; got != "75.0%" {
|
||||
t.Fatalf("Value = %q, want \"75.0%%\" (Percent wins)", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("value_arm_takes_precedence_over_used_total", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
used := int64(100)
|
||||
total := int64(200)
|
||||
metric := &unified.MetricValue{Value: 5.0, Unit: "GiB", Used: &used, Total: &total}
|
||||
addMetricFact(&facts, "Memory", metric, &observedAt)
|
||||
|
||||
if len(facts) != 1 {
|
||||
t.Fatalf("expected exactly 1 fact, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
if got := facts[0].Value; got != "5.0 GiB" {
|
||||
t.Fatalf("Value = %q, want \"5.0 GiB\" (Value wins over Used/Total)", got)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative_value_takes_value_arm", func(t *testing.T) {
|
||||
facts := []Fact{}
|
||||
metric := &unified.MetricValue{Value: -3.5, Unit: "MB/s"}
|
||||
addMetricFact(&facts, "Network out", metric, &observedAt)
|
||||
|
||||
if len(facts) != 1 {
|
||||
t.Fatalf("expected exactly 1 fact for negative Value, got %d: %+v", len(facts), facts)
|
||||
}
|
||||
if got := facts[0].Value; got != "-3.5 MB/s" {
|
||||
t.Fatalf("Value = %q, want \"-3.5 MB/s\"", got)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,117 @@
|
||||
package providers
|
||||
|
||||
import (
|
||||
"reflect"
|
||||
"testing"
|
||||
|
||||
"github.com/stretchr/testify/assert"
|
||||
)
|
||||
|
||||
// TestToolProgressEventNormalizeCollections_BranchCov0719pm exercises every
|
||||
// branch of (ToolProgressEvent).NormalizeCollections (provider.go:211):
|
||||
// - the `if e.Input == nil` true arm, which substitutes a non-nil empty map
|
||||
// - the implicit else, where a populated Input map is preserved unchanged
|
||||
//
|
||||
// Both cases also assert that the non-Input scalar/slice fields are passed
|
||||
// through verbatim so the normalizer is confirmed to be a no-op outside of the
|
||||
// nil-collection fix-up.
|
||||
func TestToolProgressEventNormalizeCollections_BranchCov0719pm(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
event ToolProgressEvent
|
||||
// wantInputIsNil reports the expected nil-ness of the returned Input
|
||||
// map. It is always false after normalization; documented per-case for
|
||||
// clarity.
|
||||
wantInputIsNil bool
|
||||
// wantInput reports the expected contents of the returned Input map
|
||||
// (compared with reflect.DeepEqual so an empty non-nil map is
|
||||
// distinguishable from a nil one).
|
||||
wantInput map[string]interface{}
|
||||
}{
|
||||
{
|
||||
name: "nil input map is replaced with non-nil empty map",
|
||||
event: ToolProgressEvent{
|
||||
ID: "tool_42",
|
||||
Name: "get_time",
|
||||
Input: nil,
|
||||
RawInput: `{"tz":"UTC"}`,
|
||||
Phase: "streaming",
|
||||
Message: "Receiving tool input.",
|
||||
},
|
||||
wantInputIsNil: false,
|
||||
wantInput: map[string]interface{}{},
|
||||
},
|
||||
{
|
||||
name: "populated input map is preserved unchanged",
|
||||
event: ToolProgressEvent{
|
||||
ID: "tool_43",
|
||||
Name: "search",
|
||||
Input: map[string]interface{}{"q": "pulse", "limit": float64(10)},
|
||||
Phase: "ready",
|
||||
},
|
||||
wantInputIsNil: false,
|
||||
wantInput: map[string]interface{}{"q": "pulse", "limit": float64(10)},
|
||||
},
|
||||
}
|
||||
|
||||
for _, tt := range tests {
|
||||
t.Run(tt.name, func(t *testing.T) {
|
||||
// Snapshot the input so we can prove the scalar/slice fields are
|
||||
// preserved on the populated case. Input is asserted separately.
|
||||
inID, inName := tt.event.ID, tt.event.Name
|
||||
inRaw, inPhase, inMsg := tt.event.RawInput, tt.event.Phase, tt.event.Message
|
||||
|
||||
got := tt.event.NormalizeCollections()
|
||||
|
||||
// Input map: must never be nil after normalization, and contents
|
||||
// must equal the expected map (empty for the nil case, the
|
||||
// original entries for the populated case).
|
||||
if got.Input == nil {
|
||||
t.Fatalf("NormalizeCollections returned nil Input map; want non-nil")
|
||||
}
|
||||
if tt.wantInputIsNil {
|
||||
t.Fatalf("test data error: wantInputIsNil must be false after normalization")
|
||||
}
|
||||
if !reflect.DeepEqual(got.Input, tt.wantInput) {
|
||||
t.Fatalf("NormalizeCollections Input = %#v, want %#v", got.Input, tt.wantInput)
|
||||
}
|
||||
|
||||
// On the populated case, the original map object identity may be
|
||||
// preserved or a new map allocated with the same contents — the
|
||||
// contract is contents-equality, asserted above via DeepEqual.
|
||||
// Here we additionally assert the non-nil invariant explicitly so
|
||||
// a future refactor that drops the fix-up fails loudly.
|
||||
assert.NotNil(t, got.Input, "normalized Input map must be non-nil")
|
||||
|
||||
// All other fields must round-trip unchanged.
|
||||
assert.Equal(t, inID, got.ID, "ID must be preserved")
|
||||
assert.Equal(t, inName, got.Name, "Name must be preserved")
|
||||
assert.Equal(t, inRaw, got.RawInput, "RawInput must be preserved")
|
||||
assert.Equal(t, inPhase, got.Phase, "Phase must be preserved")
|
||||
assert.Equal(t, inMsg, got.Message, "Message must be preserved")
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
// TestToolProgressEventNormalizeCollections_ZeroValue0719pm covers the literal
|
||||
// zero value of ToolProgressEvent, which is the most common shape that reaches
|
||||
// NormalizeCollections when a provider constructs an event before populating
|
||||
// any field. Every field — including Input — starts nil and Input must end up a
|
||||
// non-nil empty map.
|
||||
func TestToolProgressEventNormalizeCollections_ZeroValue0719pm(t *testing.T) {
|
||||
var zero ToolProgressEvent
|
||||
got := zero.NormalizeCollections()
|
||||
|
||||
if got.Input == nil {
|
||||
t.Fatalf("NormalizeCollections on zero-value event left Input nil; want non-nil empty map")
|
||||
}
|
||||
if len(got.Input) != 0 {
|
||||
t.Fatalf("NormalizeCollections on zero-value event Input len = %d, want 0", len(got.Input))
|
||||
}
|
||||
// Zero scalars are unchanged.
|
||||
assert.Equal(t, "", got.ID, "ID must remain zero value")
|
||||
assert.Equal(t, "", got.Name, "Name must remain zero value")
|
||||
assert.Equal(t, "", got.RawInput, "RawInput must remain zero value")
|
||||
assert.Equal(t, "", got.Phase, "Phase must remain zero value")
|
||||
assert.Equal(t, "", got.Message, "Message must remain zero value")
|
||||
}
|
||||
@@ -0,0 +1,218 @@
|
||||
package tools
|
||||
|
||||
import (
|
||||
"testing"
|
||||
)
|
||||
|
||||
// This test file raises branch/function coverage on the two pure
|
||||
// ToToolResponse() methods declared in tools_query.go:
|
||||
// - (*ErrStrictResolution).ToToolResponse() (tools_query.go:48)
|
||||
// - (*ErrRoutingMismatch).ToToolResponse() (tools_query.go:93)
|
||||
//
|
||||
// It deliberately exercises both arms of the conditional inside
|
||||
// ErrRoutingMismatch.ToToolResponse() (MoreSpecificIDs populated vs empty),
|
||||
// since the existing strict_resolution_test.go only hits the empty branch.
|
||||
|
||||
// TestErrStrictResolutionToToolResponse_BranchCov0719pm drives the single
|
||||
// return path of (*ErrStrictResolution).ToToolResponse() and asserts the
|
||||
// returned ToolResponse carries the STRICT_RESOLUTION code, the original
|
||||
// human-readable message, the blocked flag, and the exact metadata map the
|
||||
// policy contract publishes (resource_id / action / policy_boundary).
|
||||
func TestErrStrictResolutionToToolResponse_BranchCov0719pm(t *testing.T) {
|
||||
const (
|
||||
wantResourceID = "vm:101"
|
||||
wantAction = "restart"
|
||||
wantMessage = "resource 'vm:101' has not been discovered; discovery is required before restart"
|
||||
wantPolicy = "Resource discovery is required before resource-specific actions."
|
||||
)
|
||||
|
||||
err := &ErrStrictResolution{
|
||||
ResourceID: wantResourceID,
|
||||
Action: wantAction,
|
||||
Message: wantMessage,
|
||||
}
|
||||
|
||||
resp := err.ToToolResponse()
|
||||
|
||||
if resp.OK {
|
||||
t.Fatalf("ToToolResponse().OK = true; blocked operations must report OK=false")
|
||||
}
|
||||
if resp.Error == nil {
|
||||
t.Fatalf("ToToolResponse().Error = nil; STRICT_RESOLUTION must populate Error envelope")
|
||||
}
|
||||
if resp.Error.Code != ErrCodeStrictResolution {
|
||||
t.Fatalf("Error.Code = %q, want %q", resp.Error.Code, ErrCodeStrictResolution)
|
||||
}
|
||||
if !resp.Error.Blocked {
|
||||
t.Errorf("Error.Blocked = false, want true (STRICT_RESOLUTION is a policy block)")
|
||||
}
|
||||
if resp.Error.Failed {
|
||||
t.Errorf("Error.Failed = true, want false (block, not execution failure)")
|
||||
}
|
||||
if resp.Error.Message != wantMessage {
|
||||
t.Errorf("Error.Message = %q, want %q", resp.Error.Message, wantMessage)
|
||||
}
|
||||
|
||||
details := resp.Error.Details
|
||||
if details == nil {
|
||||
t.Fatalf("Error.Details = nil; expected populated policy metadata")
|
||||
}
|
||||
|
||||
if got, ok := details["resource_id"].(string); !ok || got != wantResourceID {
|
||||
t.Errorf("Details[resource_id] = %v, want %q", details["resource_id"], wantResourceID)
|
||||
}
|
||||
if got, ok := details["action"].(string); !ok || got != wantAction {
|
||||
t.Errorf("Details[action] = %v, want %q", details["action"], wantAction)
|
||||
}
|
||||
if got, ok := details["policy_boundary"].(string); !ok || got != wantPolicy {
|
||||
t.Errorf("Details[policy_boundary] = %v, want %q", details["policy_boundary"], wantPolicy)
|
||||
}
|
||||
|
||||
if _, present := details["more_specific_resources"]; present {
|
||||
t.Errorf("Details unexpectedly carries more_specific_resources (strict-resolution payload)")
|
||||
}
|
||||
}
|
||||
|
||||
// TestErrRoutingMismatchToToolResponse_BranchCov0719pm is table-driven over
|
||||
// both branches of the `len(e.MoreSpecificIDs) > 0` conditional in
|
||||
// (*ErrRoutingMismatch).ToToolResponse(): the canonical-ID arm (which adds
|
||||
// the more_specific_resource_ids fact) and the empty arm (which omits it).
|
||||
// Each case asserts the ROUTING_MISMATCH code, the blocked flag, the echoed
|
||||
// message, and the precise set of metadata keys/values the function emits.
|
||||
func TestErrRoutingMismatchToToolResponse_BranchCov0719pm(t *testing.T) {
|
||||
const wantPolicy = "A more specific child resource exists on the requested host; choose the intended resource before retrying a scoped action."
|
||||
|
||||
type expect struct {
|
||||
name string
|
||||
targetHost string
|
||||
moreSpecificResources []string
|
||||
moreSpecificIDs []string
|
||||
childKinds []string
|
||||
message string
|
||||
wantIDsKey bool // true → expects more_specific_resource_ids
|
||||
wantIDsValue []string
|
||||
wantResourcesAssertionType bool // true → assert Details[more_specific_resources] type-asserts as []string
|
||||
}
|
||||
|
||||
cases := []expect{
|
||||
{
|
||||
name: "with_canonical_ids_populates_more_specific_resource_ids",
|
||||
targetHost: "pve-node",
|
||||
moreSpecificResources: []string{"homepage-docker", "jellyfin"},
|
||||
moreSpecificIDs: []string{"system-container:proxmox:141", "vm:proxmox:100"},
|
||||
childKinds: []string{"system-container", "vm"},
|
||||
message: "target_host 'pve-node' has more specific children: [homepage-docker jellyfin]",
|
||||
wantIDsKey: true,
|
||||
wantIDsValue: []string{"system-container:proxmox:141", "vm:proxmox:100"},
|
||||
wantResourcesAssertionType: true,
|
||||
},
|
||||
{
|
||||
name: "without_canonical_ids_omits_more_specific_resource_ids",
|
||||
targetHost: "pve-node",
|
||||
moreSpecificResources: []string{"homepage-docker"},
|
||||
moreSpecificIDs: nil, // forces the else branch
|
||||
childKinds: nil,
|
||||
message: "target_host 'pve-node' has more specific children: [homepage-docker]",
|
||||
wantIDsKey: false,
|
||||
wantResourcesAssertionType: true,
|
||||
},
|
||||
{
|
||||
name: "empty_canonical_ids_slice_also_takes_else_branch",
|
||||
targetHost: "prox97",
|
||||
moreSpecificResources: []string{},
|
||||
moreSpecificIDs: []string{},
|
||||
childKinds: []string{},
|
||||
message: "target_host 'prox97' has more specific children: []",
|
||||
wantIDsKey: false,
|
||||
wantResourcesAssertionType: true,
|
||||
},
|
||||
}
|
||||
|
||||
for _, tc := range cases {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
err := &ErrRoutingMismatch{
|
||||
TargetHost: tc.targetHost,
|
||||
MoreSpecificResources: tc.moreSpecificResources,
|
||||
MoreSpecificIDs: tc.moreSpecificIDs,
|
||||
ChildKinds: tc.childKinds,
|
||||
Message: tc.message,
|
||||
}
|
||||
|
||||
resp := err.ToToolResponse()
|
||||
|
||||
if resp.OK {
|
||||
t.Fatalf("ToToolResponse().OK = true; routing mismatch must report OK=false")
|
||||
}
|
||||
if resp.Error == nil {
|
||||
t.Fatalf("ToToolResponse().Error = nil; ROUTING_MISMATCH must populate Error envelope")
|
||||
}
|
||||
if resp.Error.Code != ErrCodeRoutingMismatch {
|
||||
t.Fatalf("Error.Code = %q, want %q", resp.Error.Code, ErrCodeRoutingMismatch)
|
||||
}
|
||||
if !resp.Error.Blocked {
|
||||
t.Errorf("Error.Blocked = false, want true (ROUTING_MISMATCH is a policy block)")
|
||||
}
|
||||
if resp.Error.Failed {
|
||||
t.Errorf("Error.Failed = true, want false (block, not execution failure)")
|
||||
}
|
||||
if resp.Error.Message != tc.message {
|
||||
t.Errorf("Error.Message = %q, want %q", resp.Error.Message, tc.message)
|
||||
}
|
||||
|
||||
details := resp.Error.Details
|
||||
if details == nil {
|
||||
t.Fatalf("Error.Details = nil; expected populated routing metadata")
|
||||
}
|
||||
|
||||
if got, ok := details["target_host"].(string); !ok || got != tc.targetHost {
|
||||
t.Errorf("Details[target_host] = %v, want %q", details["target_host"], tc.targetHost)
|
||||
}
|
||||
|
||||
if tc.wantResourcesAssertionType {
|
||||
resources, ok := details["more_specific_resources"].([]string)
|
||||
if !ok {
|
||||
t.Fatalf("Details[more_specific_resources] type = %T, want []string", details["more_specific_resources"])
|
||||
}
|
||||
if len(resources) != len(tc.moreSpecificResources) {
|
||||
t.Fatalf("Details[more_specific_resources] = %v, want %v", resources, tc.moreSpecificResources)
|
||||
}
|
||||
for i := range resources {
|
||||
if resources[i] != tc.moreSpecificResources[i] {
|
||||
t.Errorf("Details[more_specific_resources][%d] = %q, want %q", i, resources[i], tc.moreSpecificResources[i])
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
idsValue, idsPresent := details["more_specific_resource_ids"]
|
||||
if tc.wantIDsKey {
|
||||
if !idsPresent {
|
||||
t.Fatalf("Details[more_specific_resource_ids] missing; expected to be present when MoreSpecificIDs is non-empty")
|
||||
}
|
||||
idsSlice, ok := idsValue.([]string)
|
||||
if !ok {
|
||||
t.Fatalf("Details[more_specific_resource_ids] type = %T, want []string", idsValue)
|
||||
}
|
||||
if len(idsSlice) != len(tc.wantIDsValue) {
|
||||
t.Fatalf("Details[more_specific_resource_ids] = %v, want %v", idsSlice, tc.wantIDsValue)
|
||||
}
|
||||
for i := range idsSlice {
|
||||
if idsSlice[i] != tc.wantIDsValue[i] {
|
||||
t.Errorf("Details[more_specific_resource_ids][%d] = %q, want %q", i, idsSlice[i], tc.wantIDsValue[i])
|
||||
}
|
||||
}
|
||||
} else {
|
||||
if idsPresent {
|
||||
t.Errorf("Details[more_specific_resource_ids] = %v; expected to be absent when MoreSpecificIDs is empty", idsValue)
|
||||
}
|
||||
}
|
||||
|
||||
if got, ok := details["policy_boundary"].(string); !ok || got != wantPolicy {
|
||||
t.Errorf("Details[policy_boundary] = %v, want %q", details["policy_boundary"], wantPolicy)
|
||||
}
|
||||
|
||||
if _, present := details["auto_recoverable"]; present {
|
||||
t.Errorf("Details[auto_recoverable] present; routing mismatch must not surface auto-recovery hint")
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,92 @@
|
||||
package email
|
||||
|
||||
import (
|
||||
"strings"
|
||||
"testing"
|
||||
)
|
||||
|
||||
// TestRenderMagicLinkEmail covers RenderMagicLinkEmail in templates.go.
|
||||
//
|
||||
// The function's only error arm comes from magicLinkTemplate.Execute, which is
|
||||
// unreachable at runtime: magicLinkTemplate is a package-level template.Must of
|
||||
// a constant parse string, so Execute cannot fail for the single string field
|
||||
// MagicLinkData.MagicLinkURL. That arm is therefore not fabricated here.
|
||||
func TestRenderMagicLinkEmail(t *testing.T) {
|
||||
t.Run("populated URL substituted into html and text", func(t *testing.T) {
|
||||
const url = "https://pulse.example.com/auth/magic?token=abc123XYZ"
|
||||
|
||||
htmlBody, textBody, err := RenderMagicLinkEmail(MagicLinkData{MagicLinkURL: url})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
if htmlBody == "" {
|
||||
t.Fatal("expected non-empty html body")
|
||||
}
|
||||
if textBody == "" {
|
||||
t.Fatal("expected non-empty text body")
|
||||
}
|
||||
|
||||
if !strings.Contains(htmlBody, url) {
|
||||
t.Errorf("html body does not contain magic link URL;\nwant substring: %s\ngot: %s", url, htmlBody)
|
||||
}
|
||||
if !strings.Contains(htmlBody, "Sign in to Pulse") {
|
||||
t.Errorf("html body does not contain expected heading text;\ngot: %s", htmlBody)
|
||||
}
|
||||
if !strings.Contains(htmlBody, `href="`+url+`"`) {
|
||||
t.Errorf("html body does not contain href with the URL;\ngot: %s", htmlBody)
|
||||
}
|
||||
|
||||
if !strings.Contains(textBody, url) {
|
||||
t.Errorf("text body does not contain magic link URL;\nwant substring: %s\ngot: %s", url, textBody)
|
||||
}
|
||||
if !strings.Contains(textBody, "Sign in to Pulse") {
|
||||
t.Errorf("text body does not contain expected greeting;\ngot: %s", textBody)
|
||||
}
|
||||
if !strings.Contains(textBody, "expires in 15 minutes") {
|
||||
t.Errorf("text body does not contain expiry note;\ngot: %s", textBody)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("html template escapes ampersands in URL, text body does not", func(t *testing.T) {
|
||||
const rawURL = "https://pulse.example.com/auth/magic?token=abc&uid=42"
|
||||
|
||||
htmlBody, textBody, err := RenderMagicLinkEmail(MagicLinkData{MagicLinkURL: rawURL})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error: %v", err)
|
||||
}
|
||||
|
||||
const escapedURL = "https://pulse.example.com/auth/magic?token=abc&uid=42"
|
||||
if !strings.Contains(htmlBody, escapedURL) {
|
||||
t.Errorf("html body should contain HTML-escaped URL %q;\ngot: %s", escapedURL, htmlBody)
|
||||
}
|
||||
if strings.Contains(htmlBody, `href="`+rawURL+`"`) {
|
||||
t.Errorf("html body must not contain the raw unescaped URL inside href;\ngot: %s", htmlBody)
|
||||
}
|
||||
|
||||
if !strings.Contains(textBody, rawURL) {
|
||||
t.Errorf("text body should contain the raw URL %q (no HTML escaping);\ngot: %s", rawURL, textBody)
|
||||
}
|
||||
if strings.Contains(textBody, escapedURL) {
|
||||
t.Errorf("text body must not contain HTML-escaped ampersands;\ngot: %s", textBody)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("empty URL still renders without error", func(t *testing.T) {
|
||||
htmlBody, textBody, err := RenderMagicLinkEmail(MagicLinkData{MagicLinkURL: ""})
|
||||
if err != nil {
|
||||
t.Fatalf("unexpected error for empty URL: %v", err)
|
||||
}
|
||||
if htmlBody == "" {
|
||||
t.Fatal("expected non-empty html body even with empty URL")
|
||||
}
|
||||
if textBody == "" {
|
||||
t.Fatal("expected non-empty text body even with empty URL")
|
||||
}
|
||||
if !strings.Contains(htmlBody, "Sign in to Pulse") {
|
||||
t.Errorf("html body missing heading for empty URL;\ngot: %s", htmlBody)
|
||||
}
|
||||
if !strings.Contains(textBody, "Sign in to Pulse") {
|
||||
t.Errorf("text body missing greeting for empty URL;\ngot: %s", textBody)
|
||||
}
|
||||
})
|
||||
}
|
||||
@@ -0,0 +1,185 @@
|
||||
package recovery
|
||||
|
||||
import (
|
||||
"testing"
|
||||
"time"
|
||||
)
|
||||
|
||||
func TestRecoveryDetailStringBranch0719pm(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
point RecoveryPoint
|
||||
key string
|
||||
want string
|
||||
}{
|
||||
{
|
||||
name: "nil details returns empty",
|
||||
point: RecoveryPoint{},
|
||||
key: "instance",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "populated details but key absent returns empty",
|
||||
point: RecoveryPoint{
|
||||
Details: map[string]any{
|
||||
"instance": "pve-main",
|
||||
},
|
||||
},
|
||||
key: "missing-key",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "key present but value is an int returns empty",
|
||||
point: RecoveryPoint{
|
||||
Details: map[string]any{
|
||||
"vmid": 100,
|
||||
},
|
||||
},
|
||||
key: "vmid",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "key present but value is nil returns empty",
|
||||
point: RecoveryPoint{
|
||||
Details: map[string]any{
|
||||
"connectionId": nil,
|
||||
},
|
||||
},
|
||||
key: "connectionId",
|
||||
want: "",
|
||||
},
|
||||
{
|
||||
name: "key present with bare string value returns value",
|
||||
point: RecoveryPoint{
|
||||
Details: map[string]any{
|
||||
"instance": "pve-main",
|
||||
},
|
||||
},
|
||||
key: "instance",
|
||||
want: "pve-main",
|
||||
},
|
||||
{
|
||||
name: "key present with surrounding whitespace returns trimmed value",
|
||||
point: RecoveryPoint{
|
||||
Details: map[string]any{
|
||||
"k8sClusterId": " prod-eks ",
|
||||
},
|
||||
},
|
||||
key: "k8sClusterId",
|
||||
want: "prod-eks",
|
||||
},
|
||||
{
|
||||
name: "key present with whitespace-only string returns empty",
|
||||
point: RecoveryPoint{
|
||||
Details: map[string]any{
|
||||
"instance": " ",
|
||||
},
|
||||
},
|
||||
key: "instance",
|
||||
want: "",
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := recoveryDetailString(test.point, test.key)
|
||||
if got != test.want {
|
||||
t.Fatalf("recoveryDetailString(...) = %q, want %q", got, test.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestRecoveryPointObservedAtBranch0719pm(t *testing.T) {
|
||||
t.Parallel()
|
||||
|
||||
completedAt := time.Date(2026, 7, 19, 10, 0, 0, 0, time.UTC)
|
||||
startedAt := time.Date(2026, 7, 19, 9, 0, 0, 0, time.UTC)
|
||||
// Construct a non-UTC time so we can assert the result is normalised to UTC.
|
||||
completedNonUTC := time.Date(
|
||||
2026, 7, 19, 8, 0, 0, 0, time.FixedZone("PVE", -2*3600),
|
||||
)
|
||||
zeroTime := time.Time{}
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
point RecoveryPoint
|
||||
want time.Time
|
||||
}{
|
||||
{
|
||||
name: "nil completed and nil started returns zero",
|
||||
point: RecoveryPoint{},
|
||||
want: time.Time{},
|
||||
},
|
||||
{
|
||||
name: "completed present returns completed in UTC",
|
||||
point: RecoveryPoint{
|
||||
CompletedAt: &completedAt,
|
||||
},
|
||||
want: completedAt.UTC(),
|
||||
},
|
||||
{
|
||||
name: "completed nil but started present returns started in UTC",
|
||||
point: RecoveryPoint{
|
||||
StartedAt: &startedAt,
|
||||
},
|
||||
want: startedAt.UTC(),
|
||||
},
|
||||
{
|
||||
name: "both completed and started present prefers completed",
|
||||
point: RecoveryPoint{
|
||||
CompletedAt: &completedAt,
|
||||
StartedAt: &startedAt,
|
||||
},
|
||||
want: completedAt.UTC(),
|
||||
},
|
||||
{
|
||||
name: "non-nil completed pointer holding zero value falls through to started",
|
||||
point: RecoveryPoint{
|
||||
CompletedAt: &zeroTime,
|
||||
StartedAt: &startedAt,
|
||||
},
|
||||
want: startedAt.UTC(),
|
||||
},
|
||||
{
|
||||
name: "non-nil started pointer holding zero value returns zero",
|
||||
point: RecoveryPoint{
|
||||
StartedAt: &zeroTime,
|
||||
},
|
||||
want: time.Time{},
|
||||
},
|
||||
{
|
||||
name: "non-nil completed and started both zero returns zero",
|
||||
point: RecoveryPoint{
|
||||
CompletedAt: &zeroTime,
|
||||
StartedAt: &zeroTime,
|
||||
},
|
||||
want: time.Time{},
|
||||
},
|
||||
{
|
||||
name: "non-UTC completed is normalised to UTC equivalent",
|
||||
point: RecoveryPoint{
|
||||
CompletedAt: &completedNonUTC,
|
||||
},
|
||||
want: completedNonUTC.UTC(),
|
||||
},
|
||||
}
|
||||
|
||||
for _, test := range tests {
|
||||
test := test
|
||||
t.Run(test.name, func(t *testing.T) {
|
||||
t.Parallel()
|
||||
got := recoveryPointObservedAt(test.point)
|
||||
if !got.Equal(test.want) {
|
||||
t.Fatalf(
|
||||
"recoveryPointObservedAt(...) = %v (loc %s), want %v (loc %s)",
|
||||
got, got.Location(), test.want, test.want.Location(),
|
||||
)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,284 @@
|
||||
package reporting
|
||||
|
||||
import (
|
||||
"bytes"
|
||||
"image"
|
||||
"image/color"
|
||||
"image/png"
|
||||
"math"
|
||||
"testing"
|
||||
|
||||
"github.com/go-pdf/fpdf"
|
||||
)
|
||||
|
||||
func approxEqual(a, b float64) bool {
|
||||
const eps = 1e-6
|
||||
return math.Abs(a-b) <= eps
|
||||
}
|
||||
|
||||
// encodeLogoTestPNG returns a deterministic black-opaque RGBA PNG of the
|
||||
// requested pixel dimensions. Used to drive scaledLogoSize with a real
|
||||
// fpdf-registered image so the math runs against actual Width()/Height()
|
||||
// values rather than a fabricated stub.
|
||||
func encodeLogoTestPNG(t *testing.T, w, h int) []byte {
|
||||
t.Helper()
|
||||
img := image.NewRGBA(image.Rect(0, 0, w, h))
|
||||
for y := 0; y < h; y++ {
|
||||
for x := 0; x < w; x++ {
|
||||
img.SetRGBA(x, y, color.RGBA{R: 0, G: 0, B: 0, A: 255})
|
||||
}
|
||||
}
|
||||
var buf bytes.Buffer
|
||||
if err := png.Encode(&buf, img); err != nil {
|
||||
t.Fatalf("encode png %dx%d: %v", w, h, err)
|
||||
}
|
||||
return buf.Bytes()
|
||||
}
|
||||
|
||||
// registerLogoTestImage encodes a PNG of the given pixel size and registers
|
||||
// it with a fresh fpdf instance, returning the resulting ImageInfoType.
|
||||
func registerLogoTestImage(t *testing.T, w, h int, name string) *fpdf.ImageInfoType {
|
||||
t.Helper()
|
||||
pdf := fpdf.New("P", "mm", "A4", "")
|
||||
info := pdf.RegisterImageOptionsReader(
|
||||
name,
|
||||
fpdf.ImageOptions{ImageType: "png", ReadDpi: true},
|
||||
bytes.NewReader(encodeLogoTestPNG(t, w, h)),
|
||||
)
|
||||
if info == nil {
|
||||
t.Fatalf("RegisterImageOptionsReader returned nil info for %dx%d", w, h)
|
||||
}
|
||||
if info.Width() <= 0 || info.Height() <= 0 {
|
||||
t.Fatalf("registered image %dx%d reported non-positive extent w=%v h=%v",
|
||||
w, h, info.Width(), info.Height())
|
||||
}
|
||||
return info
|
||||
}
|
||||
|
||||
func TestReportLogoTypeFromData_BranchCov(t *testing.T) {
|
||||
pngMagic8 := []byte{0x89, 'P', 'N', 'G', '\r', '\n', 0x1a, '\n'}
|
||||
pngTail := []byte{0, 0, 0, 13, 'I', 'H', 'D', 'R'}
|
||||
pngFull := append(append([]byte{}, pngMagic8...), pngTail...)
|
||||
jpgMagic3 := []byte{0xff, 0xd8, 0xff}
|
||||
jpgFull := append(append([]byte{}, jpgMagic3...), 0xe0, 0, 0x10, 0, 0x4a, 0x46, 0x49, 0x46)
|
||||
gif87a := []byte("GIF87a")
|
||||
gif89a := []byte("GIF89a")
|
||||
unknown := []byte("not an image magic at all")
|
||||
|
||||
tests := []struct {
|
||||
name string
|
||||
data []byte
|
||||
configured string
|
||||
want string
|
||||
}{
|
||||
// --- configured short-circuit arm (normalizeLogoFormat != "") ---
|
||||
{"configured png wins over jpg data", jpgFull, "png", "png"},
|
||||
{"configured jpg wins over png data", pngFull, "jpg", "jpg"},
|
||||
{"configured jpeg normalizes to jpg", pngFull, "jpeg", "jpg"},
|
||||
{"configured gif wins over png data", pngFull, "gif", "gif"},
|
||||
{"configured uppercase is normalized", jpgFull, "PNG", "png"},
|
||||
{"configured with whitespace trimmed", jpgFull, " jpg ", "jpg"},
|
||||
{"configured invalid falls through to png data", pngFull, "svg", "png"},
|
||||
{"configured invalid falls through to jpg data", jpgFull, "bmp", "jpg"},
|
||||
{"configured invalid falls through to gif data", gif87a, "weird", "gif"},
|
||||
|
||||
// --- data-detection arms with empty configured ---
|
||||
{"empty configured + full png magic", pngFull, "", "png"},
|
||||
{"empty configured + exactly 8 png magic bytes", pngMagic8, "", "png"},
|
||||
{"empty configured + full jpg magic", jpgFull, "", "jpg"},
|
||||
{"empty configured + exactly 3 jpg magic bytes", jpgMagic3, "", "jpg"},
|
||||
{"empty configured + gif87a", gif87a, "", "gif"},
|
||||
{"empty configured + gif89a", gif89a, "", "gif"},
|
||||
{"empty configured + exactly 6 gif bytes (87a)", []byte("GIF87a"), "", "gif"},
|
||||
{"empty configured + exactly 6 gif bytes (89a)", []byte("GIF89a"), "", "gif"},
|
||||
|
||||
// --- default arm (no match) ---
|
||||
{"empty configured + unknown bytes", unknown, "", ""},
|
||||
{"empty configured + nil data", nil, "", ""},
|
||||
{"empty configured + empty data", []byte{}, "", ""},
|
||||
{"png prefix too short (7 bytes)", pngMagic8[:7], "", ""},
|
||||
{"jpg prefix too short (2 bytes)", jpgMagic3[:2], "", ""},
|
||||
{"gif prefix too short (5 bytes)", gif87a[:5], "", ""},
|
||||
{"gif89a prefix wrong last char", []byte("GIF89b"), "", ""},
|
||||
{"gif87a prefix wrong last char", []byte("GIF87b"), "", ""},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := reportLogoTypeFromData(tc.data, tc.configured)
|
||||
if got != tc.want {
|
||||
t.Fatalf("reportLogoTypeFromData(%v, %q) = %q, want %q",
|
||||
tc.data, tc.configured, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestReportLogoTypeFromPath_BranchCov(t *testing.T) {
|
||||
tests := []struct {
|
||||
name string
|
||||
path string
|
||||
configured string
|
||||
want string
|
||||
}{
|
||||
// --- configured short-circuit arm (normalizeLogoFormat != "") ---
|
||||
{"configured png wins regardless of path", "/any/thing.xyz", "png", "png"},
|
||||
{"configured jpg wins regardless of path", "/any/thing", "jpg", "jpg"},
|
||||
{"configured jpeg normalizes to jpg", "/any/thing", "jpeg", "jpg"},
|
||||
{"configured gif wins regardless of path", "/any/thing", "gif", "gif"},
|
||||
{"configured uppercase normalized to png", "/any/thing", "PNG", "png"},
|
||||
{"configured whitespace trimmed to jpg", "/any/thing", " jpg ", "jpg"},
|
||||
{"configured invalid falls through to extension png", "/a.png", "svg", "png"},
|
||||
{"configured invalid falls through to default arm", "/a.svg", "svg", ""},
|
||||
|
||||
// --- extension switch arms with empty configured ---
|
||||
{"png extension lowercase", "logo.png", "", "png"},
|
||||
{"png extension uppercase", "LOGO.PNG", "", "png"},
|
||||
{"png extension mixed case", "Logo.PnG", "", "png"},
|
||||
{"jpg extension", "logo.jpg", "", "jpg"},
|
||||
{"jpeg extension", "logo.jpeg", "", "jpg"},
|
||||
{"JPG extension uppercase", "LOGO.JPG", "", "jpg"},
|
||||
{"JPEG extension uppercase", "LOGO.JPEG", "", "jpg"},
|
||||
{"gif extension", "logo.gif", "", "gif"},
|
||||
{"GIF extension uppercase", "LOGO.GIF", "", "gif"},
|
||||
|
||||
// --- default arm (unknown / no extension) ---
|
||||
{"unknown svg extension", "logo.svg", "", ""},
|
||||
{"unknown bmp extension", "logo.bmp", "", ""},
|
||||
{"unknown webp extension", "logo.webp", "", ""},
|
||||
{"unknown txt extension", "logo.txt", "", ""},
|
||||
{"no extension at all", "README", "", ""},
|
||||
{"trailing dot only", "logo.", "", ""},
|
||||
{"trailing dot uppercased", "LOGO.", "", ""},
|
||||
{"png with trailing wrong ext", "logo.png.bak", "", ""},
|
||||
|
||||
// --- realistic full paths ---
|
||||
{"absolute path png", "/var/lib/pulse/assets/brand.png", "", "png"},
|
||||
{"url-style path jpg", "https://example.com/img/logo.jpg", "", "jpg"},
|
||||
{"windows-style path gif", `C:\assets\logo.gif`, "", "gif"},
|
||||
{"empty path with empty configured", "", "", ""},
|
||||
}
|
||||
|
||||
for _, tc := range tests {
|
||||
t.Run(tc.name, func(t *testing.T) {
|
||||
got := reportLogoTypeFromPath(tc.path, tc.configured)
|
||||
if got != tc.want {
|
||||
t.Fatalf("reportLogoTypeFromPath(%q, %q) = %q, want %q",
|
||||
tc.path, tc.configured, got, tc.want)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestScaledLogoSize_BranchCov(t *testing.T) {
|
||||
const eps = 1e-6
|
||||
|
||||
t.Run("nil_info_returns_zero_zero", func(t *testing.T) {
|
||||
w, h := scaledLogoSize(nil, 100, 100)
|
||||
if w != 0 || h != 0 {
|
||||
t.Fatalf("scaledLogoSize(nil, 100, 100) = (%v, %v), want (0, 0)", w, h)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero_maxW_hits_scale_zero_arm", func(t *testing.T) {
|
||||
info := registerLogoTestImage(t, 8, 4, "zero-maxw")
|
||||
w, h := scaledLogoSize(info, 0, 100)
|
||||
if w != 0 || h != 0 {
|
||||
t.Fatalf("scaledLogoSize(info, 0, 100) = (%v, %v), want (0, 0)", w, h)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("zero_maxH_hits_scale_zero_arm", func(t *testing.T) {
|
||||
info := registerLogoTestImage(t, 8, 4, "zero-maxh")
|
||||
w, h := scaledLogoSize(info, 100, 0)
|
||||
if w != 0 || h != 0 {
|
||||
t.Fatalf("scaledLogoSize(info, 100, 0) = (%v, %v), want (0, 0)", w, h)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative_maxW_hits_scale_zero_arm", func(t *testing.T) {
|
||||
info := registerLogoTestImage(t, 8, 4, "neg-maxw")
|
||||
w, h := scaledLogoSize(info, -10, 100)
|
||||
if w != 0 || h != 0 {
|
||||
t.Fatalf("scaledLogoSize(info, -10, 100) = (%v, %v), want (0, 0)", w, h)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("negative_maxH_hits_scale_zero_arm", func(t *testing.T) {
|
||||
info := registerLogoTestImage(t, 8, 4, "neg-maxh")
|
||||
w, h := scaledLogoSize(info, 100, -10)
|
||||
if w != 0 || h != 0 {
|
||||
t.Fatalf("scaledLogoSize(info, 100, -10) = (%v, %v), want (0, 0)", w, h)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("square_image_square_box_fills_both_axes", func(t *testing.T) {
|
||||
info := registerLogoTestImage(t, 4, 4, "square")
|
||||
const maxW, maxH = 100.0, 100.0
|
||||
w, h := scaledLogoSize(info, maxW, maxH)
|
||||
if !approxEqual(w, 100) || !approxEqual(h, 100) {
|
||||
t.Fatalf("scaledLogoSize(square 4x4, 100, 100) = (%v, %v), want (100, 100)", w, h)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wide_image_in_square_box_width_binds", func(t *testing.T) {
|
||||
// 8x4 aspect 2:1 in a 1:1 box: width binds, output is (100, 50).
|
||||
info := registerLogoTestImage(t, 8, 4, "wide-sqbox")
|
||||
const maxW, maxH = 100.0, 100.0
|
||||
w, h := scaledLogoSize(info, maxW, maxH)
|
||||
if !approxEqual(w, 100) || !approxEqual(h, 50) {
|
||||
t.Fatalf("scaledLogoSize(8x4, 100, 100) = (%v, %v), want (100, 50)", w, h)
|
||||
}
|
||||
if w > maxW+eps || h > maxH+eps {
|
||||
t.Fatalf("output (%v, %v) exceeds box (%v, %v)", w, h, maxW, maxH)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tall_image_in_square_box_height_binds", func(t *testing.T) {
|
||||
// 4x8 aspect 1:2 in a 1:1 box: height binds, output is (50, 100).
|
||||
info := registerLogoTestImage(t, 4, 8, "tall-sqbox")
|
||||
const maxW, maxH = 100.0, 100.0
|
||||
w, h := scaledLogoSize(info, maxW, maxH)
|
||||
if !approxEqual(w, 50) || !approxEqual(h, 100) {
|
||||
t.Fatalf("scaledLogoSize(4x8, 100, 100) = (%v, %v), want (50, 100)", w, h)
|
||||
}
|
||||
if w > maxW+eps || h > maxH+eps {
|
||||
t.Fatalf("output (%v, %v) exceeds box (%v, %v)", w, h, maxW, maxH)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("wide_image_in_matched_aspect_box_fills_both", func(t *testing.T) {
|
||||
// 8x4 aspect 2:1 in a 100x50 box (also 2:1): both axes bind at once.
|
||||
info := registerLogoTestImage(t, 8, 4, "wide-matched")
|
||||
const maxW, maxH = 100.0, 50.0
|
||||
w, h := scaledLogoSize(info, maxW, maxH)
|
||||
if !approxEqual(w, 100) || !approxEqual(h, 50) {
|
||||
t.Fatalf("scaledLogoSize(8x4, 100, 50) = (%v, %v), want (100, 50)", w, h)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("tall_image_in_matched_aspect_box_fills_both", func(t *testing.T) {
|
||||
// 4x8 aspect 1:2 in a 50x100 box (also 1:2): both axes bind at once.
|
||||
info := registerLogoTestImage(t, 4, 8, "tall-matched")
|
||||
const maxW, maxH = 50.0, 100.0
|
||||
w, h := scaledLogoSize(info, maxW, maxH)
|
||||
if !approxEqual(w, 50) || !approxEqual(h, 100) {
|
||||
t.Fatalf("scaledLogoSize(4x8, 50, 100) = (%v, %v), want (50, 100)", w, h)
|
||||
}
|
||||
})
|
||||
|
||||
t.Run("image_smaller_than_box_uses_uniform_scale", func(t *testing.T) {
|
||||
// Verify aspect ratio is preserved across a non-square box where the
|
||||
// image is upscaling rather than downscaling. 6x2 aspect 3:1 in a
|
||||
// 300x100 box (also 3:1): both axes bind.
|
||||
info := registerLogoTestImage(t, 6, 2, "upscale")
|
||||
const maxW, maxH = 300.0, 100.0
|
||||
w, h := scaledLogoSize(info, maxW, maxH)
|
||||
if !approxEqual(w, 300) || !approxEqual(h, 100) {
|
||||
t.Fatalf("scaledLogoSize(6x2, 300, 100) = (%v, %v), want (300, 100)", w, h)
|
||||
}
|
||||
if w > maxW+eps || h > maxH+eps {
|
||||
t.Fatalf("output (%v, %v) exceeds box (%v, %v)", w, h, maxW, maxH)
|
||||
}
|
||||
})
|
||||
}
|
||||
Reference in New Issue
Block a user