cozystack/internal/controller/dashboard/factory_test.go
Aleksei Sviridkin 1bae0775be
feat(application): add WorkloadsReady condition and Events tab
Add two features to improve application observability in the dashboard:

1. WorkloadsReady condition on Application status
   - Query WorkloadMonitor resources to determine if all pods are running
   - Expose WorkloadsReady as a separate condition alongside Ready
   - Ready continues to reflect HelmRelease state only (no override) to
     preserve backward compatibility with tooling and avoid false-negatives
     during normal startup windows when pods are still coming up
   - Handle three states: operational, not operational, unknown (pending)
   - Fail-open on WorkloadMonitor query errors
   - Integrate with Application Watch to emit MODIFIED events on
     WorkloadMonitor changes (with initial-events-end safety)
   - Register cozystack.io/v1alpha1 types in API server scheme

2. Events tab in dashboard
   - Show Kubernetes Events scoped to application namespace
   - Use status.namespace for Tenant applications
   - Include both lastTimestamp and eventTime columns for K8s compat

3. Bug fix: WorkloadMonitor Operational status persistence
   - Operational was written to stale 'monitor' instead of 'fresh'
     inside RetryOnConflict, so it was never persisted to the cluster

Closes #2359
Closes #2360

Assisted-By: Claude <noreply@anthropic.com>
Signed-off-by: Aleksei Sviridkin <f@lex.la>
2026-04-15 17:20:45 +03:00

60 lines
1.7 KiB
Go

package dashboard
import (
"testing"
)
func TestEventsTab_Structure(t *testing.T) {
tab := eventsTab("PostgreSQL")
if tab["key"] != "events" {
t.Errorf("expected key=events, got %v", tab["key"])
}
if tab["label"] != "Events" {
t.Errorf("expected label=Events, got %v", tab["label"])
}
children, ok := tab["children"].([]any)
if !ok || len(children) != 1 {
t.Fatal("expected exactly 1 child in events tab")
}
table, ok := children[0].(map[string]any)
if !ok {
t.Fatal("child is not a map")
}
if table["type"] != "EnrichedTable" {
t.Errorf("expected type=EnrichedTable, got %v", table["type"])
}
data, ok := table["data"].(map[string]any)
if !ok {
t.Fatal("table data is not a map")
}
if data["id"] != "events-table" {
t.Errorf("expected id=events-table, got %v", data["id"])
}
if data["fetchUrl"] != "/api/clusters/{2}/k8s/api/v1/namespaces/{3}/events" {
t.Errorf("unexpected fetchUrl for non-Tenant: %v", data["fetchUrl"])
}
if data["customizationId"] != "factory-details-events" {
t.Errorf("expected customizationId=factory-details-events, got %v", data["customizationId"])
}
pathToItems, ok := data["pathToItems"].([]any)
if !ok || len(pathToItems) != 1 || pathToItems[0] != "items" {
t.Errorf("expected pathToItems=[items], got %v", data["pathToItems"])
}
}
func TestEventsTab_TenantUsesStatusNamespace(t *testing.T) {
tab := eventsTab("Tenant")
children := tab["children"].([]any)
table := children[0].(map[string]any)
data := table["data"].(map[string]any)
expectedURL := "/api/clusters/{2}/k8s/api/v1/namespaces/{reqsJsonPath[0]['.status.namespace']}/events"
if data["fetchUrl"] != expectedURL {
t.Errorf("expected Tenant fetchUrl to use status.namespace, got %v", data["fetchUrl"])
}
}