Public source validation / validate (push) Failing after 3m8s
211 lines
9.0 KiB
Go
211 lines
9.0 KiB
Go
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)
|
|
}
|
|
}
|