This commit is contained in:
@@ -0,0 +1,210 @@
|
||||
package agentsource
|
||||
|
||||
import (
|
||||
"context"
|
||||
"errors"
|
||||
"testing"
|
||||
"time"
|
||||
|
||||
"github.com/itworx/pulse/internal/application"
|
||||
"github.com/itworx/pulse/internal/container"
|
||||
"github.com/itworx/pulse/internal/service"
|
||||
)
|
||||
|
||||
type stubContainers struct {
|
||||
snapshot container.Snapshot
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubContainers) Snapshot(context.Context) (container.Snapshot, error) {
|
||||
return s.snapshot, s.err
|
||||
}
|
||||
|
||||
type stubServices struct {
|
||||
snapshot service.Snapshot
|
||||
err error
|
||||
}
|
||||
|
||||
func (s stubServices) Snapshot(context.Context) (service.Snapshot, error) { return s.snapshot, s.err }
|
||||
|
||||
func containerSnapshot(t *testing.T, now time.Time, containers ...container.RawContainer) container.Snapshot {
|
||||
t.Helper()
|
||||
snapshot, err := container.Normalize(container.RawSnapshot{
|
||||
Source: container.Source{ID: "container", Type: "agent"},
|
||||
Containers: containers,
|
||||
ObservedAt: now, ReceivedAt: now,
|
||||
}, now, container.Limits{})
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
return snapshot
|
||||
}
|
||||
|
||||
func TestApplicationProviderRequiresFreshContainers(t *testing.T) {
|
||||
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
||||
services := service.Snapshot{ContractVersion: service.ContractVersion, ObservedAt: now, Services: []service.ServiceStatus{}}
|
||||
|
||||
cases := []struct {
|
||||
name string
|
||||
containers container.Provider
|
||||
services service.Provider
|
||||
reason string
|
||||
}{
|
||||
{"no container provider", nil, stubServices{snapshot: services}, ReasonUnavailable},
|
||||
{"container read fails", stubContainers{err: errors.New("boom")}, stubServices{snapshot: services}, ReasonUnavailable},
|
||||
{"containers unavailable", stubContainers{snapshot: container.UnknownSnapshot(now, "container", "agent", ReasonUnavailable)}, stubServices{snapshot: services}, ReasonUnavailable},
|
||||
{"containers invalid", stubContainers{snapshot: container.UnknownSnapshot(now, "container", "agent", ReasonInvalid)}, stubServices{snapshot: services}, ReasonInvalid},
|
||||
{"containers stale", stubContainers{snapshot: container.UnknownSnapshot(now, "container", "agent", ReasonStale)}, stubServices{snapshot: services}, ReasonStale},
|
||||
}
|
||||
for _, testCase := range cases {
|
||||
t.Run(testCase.name, func(t *testing.T) {
|
||||
provider := ApplicationProvider{Containers: testCase.containers, Services: testCase.services, Now: fixedClock(now)}
|
||||
snapshot, err := provider.Snapshot(context.Background())
|
||||
if err != nil {
|
||||
t.Fatalf("unusable input must not fail the request: %v", err)
|
||||
}
|
||||
if snapshot.Source.State != "unknown" || snapshot.Source.Reason != testCase.reason {
|
||||
t.Fatalf("source = %+v, want unknown/%s", snapshot.Source, testCase.reason)
|
||||
}
|
||||
for _, item := range snapshot.Applications {
|
||||
if item.Status == application.StateHealthy {
|
||||
t.Fatal("an application became healthy without complete evidence, violating ADR-0008")
|
||||
}
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationProviderUsesContainersWhenProbesAreDisabled(t *testing.T) {
|
||||
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
||||
containers := containerSnapshot(t, now, container.RawContainer{ID: "pulse-1", Name: "pulse-api", State: "running", Health: "healthy"})
|
||||
for name, services := range map[string]service.Provider{
|
||||
"not configured": nil,
|
||||
"unavailable": stubServices{snapshot: service.UnknownSnapshot(now, "source_unavailable")},
|
||||
"read failure": stubServices{err: errors.New("probe store unavailable")},
|
||||
} {
|
||||
t.Run(name, func(t *testing.T) {
|
||||
snapshot, err := (ApplicationProvider{Containers: stubContainers{snapshot: containers}, Services: services, Now: fixedClock(now)}).Snapshot(context.Background())
|
||||
if err != nil || snapshot.Total != 1 || snapshot.Applications[0].Status != application.StateHealthy {
|
||||
t.Fatalf("snapshot=%+v err=%v", snapshot, err)
|
||||
}
|
||||
})
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationProviderGroupsContainersAndAppliesProbes(t *testing.T) {
|
||||
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
||||
containers := containerSnapshot(t, now,
|
||||
container.RawContainer{ID: "web-1", Name: "media-web", State: "running", Health: "healthy", Project: "media"},
|
||||
container.RawContainer{ID: "db-1", Name: "media-db", State: "running", Health: "healthy", Project: "media"},
|
||||
container.RawContainer{ID: "solo-1", Name: "standalone", State: "running", Health: "healthy"},
|
||||
container.RawContainer{ID: "off-1", Name: "archived", State: "exited", IntentionalStop: true},
|
||||
container.RawContainer{ID: "bad-1", Name: "broken", State: "exited"},
|
||||
)
|
||||
services := service.Snapshot{ContractVersion: service.ContractVersion, ObservedAt: now, Services: []service.ServiceStatus{
|
||||
{ID: "svc-web", Name: "media-web", State: service.StateUp},
|
||||
{ID: "svc-db", Name: "media-db", State: service.StateDown},
|
||||
}}
|
||||
provider := ApplicationProvider{Containers: stubContainers{snapshot: containers}, Services: stubServices{snapshot: services}, Now: fixedClock(now)}
|
||||
snapshot, err := provider.Snapshot(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if snapshot.Source.State != "healthy" || snapshot.Source.Reason != "" {
|
||||
t.Fatalf("unexpected source %+v", snapshot.Source)
|
||||
}
|
||||
byName := make(map[string]application.Application, len(snapshot.Applications))
|
||||
for _, item := range snapshot.Applications {
|
||||
byName[item.Name] = item
|
||||
}
|
||||
if len(byName) != 4 {
|
||||
t.Fatalf("expected one application per compose project or standalone container, got %d: %+v", len(byName), snapshot.Applications)
|
||||
}
|
||||
media, ok := byName["media"]
|
||||
if !ok || len(media.Components) != 2 {
|
||||
t.Fatalf("media project was not grouped: %+v", byName)
|
||||
}
|
||||
if media.Status != application.StateDegraded {
|
||||
t.Fatalf("a down probe on a critical component must degrade the application, got %s", media.Status)
|
||||
}
|
||||
if standalone := byName["standalone"]; standalone.Status != application.StateHealthy {
|
||||
t.Fatalf("a running container without a probe must stay healthy, got %s", standalone.Status)
|
||||
}
|
||||
if archived := byName["archived"]; archived.Status != application.StateUnknown {
|
||||
t.Fatalf("an intentionally stopped critical component must remain explicit unknown, got %s", archived.Status)
|
||||
}
|
||||
if broken := byName["broken"]; broken.Status != application.StateDegraded {
|
||||
t.Fatalf("an unexpectedly stopped container must not read as healthy, got %s", broken.Status)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationProviderBoundsTheInventory(t *testing.T) {
|
||||
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
||||
raw := make([]container.RawContainer, 0, 4)
|
||||
for _, name := range []string{"a", "b", "c", "d"} {
|
||||
raw = append(raw, container.RawContainer{ID: name, Name: name, State: "running", Health: "healthy"})
|
||||
}
|
||||
provider := ApplicationProvider{
|
||||
Containers: stubContainers{snapshot: containerSnapshot(t, now, raw...)},
|
||||
Services: stubServices{snapshot: service.Snapshot{ContractVersion: service.ContractVersion, ObservedAt: now}},
|
||||
MaxApplications: 2,
|
||||
Now: fixedClock(now),
|
||||
}
|
||||
snapshot, err := provider.Snapshot(context.Background())
|
||||
if err != nil {
|
||||
t.Fatal(err)
|
||||
}
|
||||
if snapshot.Source.State != "unknown" || snapshot.Source.Reason != ReasonInvalid {
|
||||
t.Fatalf("an oversized inventory must resolve to unknown, got %+v", snapshot.Source)
|
||||
}
|
||||
}
|
||||
|
||||
func TestApplicationProviderPropagatesCancellation(t *testing.T) {
|
||||
now := time.Date(2026, 8, 4, 12, 0, 0, 0, time.UTC)
|
||||
ctx, cancel := context.WithCancel(context.Background())
|
||||
cancel()
|
||||
provider := ApplicationProvider{
|
||||
Containers: stubContainers{err: context.Canceled},
|
||||
Services: stubServices{},
|
||||
Now: fixedClock(now),
|
||||
}
|
||||
if _, err := provider.Snapshot(ctx); !errors.Is(err, context.Canceled) {
|
||||
t.Fatalf("expected cancellation, got %v", err)
|
||||
}
|
||||
}
|
||||
|
||||
func TestContainerStateMappingNeverInventsHealth(t *testing.T) {
|
||||
cases := map[string]application.State{
|
||||
"running": application.StateHealthy,
|
||||
"restarting": application.StateDegraded,
|
||||
"paused": application.StateDegraded,
|
||||
"exited": application.StateDown,
|
||||
"dead": application.StateDown,
|
||||
"created": application.StateUnknown,
|
||||
"": application.StateUnknown,
|
||||
"weird": application.StateUnknown,
|
||||
}
|
||||
for state, want := range cases {
|
||||
if got := containerState(container.Container{State: state, Health: "healthy"}); got != want {
|
||||
t.Fatalf("container state %q mapped to %s, want %s", state, got, want)
|
||||
}
|
||||
}
|
||||
if got := containerState(container.Container{State: "running", Health: "unhealthy"}); got != application.StateDegraded {
|
||||
t.Fatalf("an unhealthy running container mapped to %s", got)
|
||||
}
|
||||
if got := containerState(container.Container{State: "running", Health: "starting"}); got != application.StateUnknown {
|
||||
t.Fatalf("a starting container mapped to %s", got)
|
||||
}
|
||||
if got := containerState(container.Container{State: "RUNNING", Health: "HEALTHY"}); got != application.StateHealthy {
|
||||
t.Fatalf("uppercase runtime state mapped to %s", got)
|
||||
}
|
||||
if got := containerState(container.Container{State: "running", Health: "unknown"}); got != application.StateUnknown {
|
||||
t.Fatalf("missing health became %s", got)
|
||||
}
|
||||
if got := containerState(container.Container{State: "exited", Health: "unknown", IntentionalStop: true}); got != application.StateUnknown {
|
||||
t.Fatalf("intentional stop became %s", got)
|
||||
}
|
||||
if got := serviceState("nonsense"); got != application.StateUnknown {
|
||||
t.Fatalf("an unrecognized probe verdict mapped to %s", got)
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user