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) } }