Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
75 changes: 75 additions & 0 deletions pkg/agent/init_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,75 @@
package agent

import (
"testing"

"github.com/kubestellar/console/pkg/ai"
)

// TestInit_WiresAIPackageFunctionPointers verifies that the package-level
// init() function in init.go correctly wires up the function pointer
// variables in the ai package. These pointers allow pkg/ai to call back
// into pkg/agent without creating an import cycle.
func TestInit_WiresAIPackageFunctionPointers(t *testing.T) {
t.Parallel()

// By the time any test in this package runs, init() has already
// executed. All four function pointers should be non-nil.

if ai.GetRegistry == nil {
t.Error("ai.GetRegistry is nil — init() did not wire it up")
}
if ai.InitializeProviders == nil {
t.Error("ai.InitializeProviders is nil — init() did not wire it up")
}
if ai.SetClusterContextProviders == nil {
t.Error("ai.SetClusterContextProviders is nil — init() did not wire it up")
}
if ai.GetConfigManager == nil {
t.Error("ai.GetConfigManager is nil — init() did not wire it up")
}
}

// TestInit_GetRegistryReturnsNonNil verifies that the wired GetRegistry
// function returns a valid (non-nil) registry instance.
func TestInit_GetRegistryReturnsNonNil(t *testing.T) {
t.Parallel()

if ai.GetRegistry == nil {
t.Skip("ai.GetRegistry not wired")
}

reg := ai.GetRegistry()
if reg == nil {
t.Error("ai.GetRegistry() returned nil")
}
}

// TestInit_GetConfigManagerReturnsNonNil verifies that the wired
// GetConfigManager function returns a valid (non-nil) config manager.
func TestInit_GetConfigManagerReturnsNonNil(t *testing.T) {
t.Parallel()

if ai.GetConfigManager == nil {
t.Skip("ai.GetConfigManager not wired")
}

mgr := ai.GetConfigManager()
if mgr == nil {
t.Error("ai.GetConfigManager() returned nil")
}
}

// TestInit_SetClusterContextProvidersDoesNotPanic verifies that calling
// SetClusterContextProviders with nil arguments does not panic (graceful
// no-op for unconfigured environments).
func TestInit_SetClusterContextProvidersDoesNotPanic(t *testing.T) {
t.Parallel()

if ai.SetClusterContextProviders == nil {
t.Skip("ai.SetClusterContextProviders not wired")
}

// Should not panic with nil args
ai.SetClusterContextProviders(nil, nil)
}
225 changes: 225 additions & 0 deletions pkg/agent/server_routes_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,225 @@
package agent

import (
"net/http"
"net/http/httptest"
"testing"
)

// expectedRoutes is the full list of patterns registered by registerRoutes.
// If a new route is added to server_routes.go but not here, this test fails
// — forcing the author to verify the route is intentional.
var expectedRoutes = []string{
"/health",
"/status",
"/clusters",
"/gpu-nodes",
"/gpu-nodes/stream",
"/nodes",
"/nodes/stream",
"/pods",
"/pods/stream",
"/events",
"/events/stream",
"/namespaces",
"/deployments",
"/replicasets",
"/statefulsets",
"/daemonsets",
"/cronjobs",
"/ingresses",
"/networkpolicies",
"/services",
"/configmaps",
"/secrets",
"/serviceaccounts",
"/jobs",
"/jobs/stream",
"/hpas",
"/pvcs",
"/pvs",
"/cluster-health",
"/roles",
"/rolebindings",
"/resourcequotas",
"/limitranges",
"/resolve-deps",
"/scale",
"/workloads/deploy",
"/workloads/delete",
"/serviceexports",
"/cilium-status",
"/jaeger-status",
"/helm/rollback",
"/helm/uninstall",
"/helm/upgrade",
"/console-cr/workloads",
"/console-cr/groups",
"/console-cr/deployments",
"/console-cr/deployments/status",
"/federation/detect",
"/federation/clusters",
"/federation/groups",
"/federation/pending-joins",
"/federation/action",
"/gitops/detect-drift",
"/gitops/sync",
"/argocd/sync",
"/gpu-health-cronjob",
"/nvidia-operators",
"/rbac/can-i",
"/rbac/permissions",
"/permissions/summary",
"/rename-context",
"/kubeconfig/preview",
"/kubeconfig/import",
"/kubeconfig/add",
"/kubeconfig/test",
"/kubeconfig/remove",
"/settings/keys",
"/settings/keys/",
"/settings",
"/settings/export",
"/settings/import",
"/providers/health",
"/provider/check",
"/predictions/ai",
"/predictions/analyze",
"/predictions/feedback",
"/predictions/stats",
"/insights/enrich",
"/insights/ai",
"/devices/alerts",
"/devices/alerts/clear",
"/devices/inventory",
"/metrics/history",
"/kagenti/agents",
"/kagenti/builds",
"/kagenti/cards",
"/kagenti/tools",
"/kagenti/summary",
"/kagent-crds/agents",
"/kagent-crds/tools",
"/kagent-crds/models",
"/kagent-crds/memories",
"/kagent-crds/summary",
"/cloud-cli-status",
"/local-cluster-tools",
"/local-clusters",
"/local-cluster-lifecycle",
"/vcluster/list",
"/vcluster/create",
"/vcluster/connect",
"/vcluster/disconnect",
"/vcluster/delete",
"/vcluster/check",
"/cancel-chat",
"/restart-backend",
"/auto-update/config",
"/auto-update/status",
"/auto-update/trigger",
"/auto-update/cancel",
"/prometheus/query",
"/metrics",
"/ws",
"/ws/exec",
}

// TestRegisterRoutes_AllExpectedRoutesRegistered verifies that registerRoutes
// registers a handler for every expected path. We call registerRoutes on a
// real ServeMux and then issue a GET to each expected path; any path that
// returns 404 is missing from the registration.
func TestRegisterRoutes_AllExpectedRoutesRegistered(t *testing.T) {
t.Parallel()

// Build a minimal server — handlers will likely panic if called, but
// we only need the mux pattern matching to work; we never invoke the
// handler logic.
s := &Server{
allowedOrigins: []string{"http://localhost"},
}

mux := http.NewServeMux()
s.registerRoutes(mux)

for _, route := range expectedRoutes {
t.Run(route, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, route, nil)
// ServeMux.Handler returns the handler and the matched pattern.
_, pattern := mux.Handler(req)
if pattern == "" {
t.Errorf("route %q is not registered in registerRoutes", route)
}
Comment on lines +147 to +152
})
}
}

// TestRegisterRoutes_CatchallReturns404ForUnknownPaths verifies that the
// catch-all "/" handler returns 404 for arbitrary unknown paths.
func TestRegisterRoutes_CatchallReturns404ForUnknownPaths(t *testing.T) {
t.Parallel()

s := &Server{
allowedOrigins: []string{"http://localhost"},
}

mux := http.NewServeMux()
s.registerRoutes(mux)

unknownPaths := []string{
"/nonexistent",
"/api/v1/pods",
"/admin/secret",
}

for _, path := range unknownPaths {
t.Run(path, func(t *testing.T) {
req := httptest.NewRequest(http.MethodGet, path, nil)
req.Header.Set("Origin", "http://localhost")
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)

if rr.Code != http.StatusNotFound {
t.Errorf("expected 404 for unknown path %q, got %d", path, rr.Code)
}
})
}
}

// TestRegisterRoutes_CatchallHandlesOPTIONS verifies that the catch-all "/"
// handler responds to OPTIONS preflight with 204 No Content.
func TestRegisterRoutes_CatchallHandlesOPTIONS(t *testing.T) {
t.Parallel()

s := &Server{
allowedOrigins: []string{"http://localhost"},
}

mux := http.NewServeMux()
s.registerRoutes(mux)

req := httptest.NewRequest(http.MethodOptions, "/unknown-path", nil)
req.Header.Set("Origin", "http://localhost")
rr := httptest.NewRecorder()
mux.ServeHTTP(rr, req)

if rr.Code != http.StatusNoContent {
t.Errorf("expected 204 for OPTIONS on catch-all, got %d", rr.Code)
}
}

// TestRegisterRoutes_RouteCount guards against accidentally removing routes
// during refactoring. If the number of registered routes drops below the
// expected count, the refactor likely lost a route.
func TestRegisterRoutes_RouteCount(t *testing.T) {
t.Parallel()

// The expected count should match len(expectedRoutes). Update both when
// adding or removing routes intentionally.
const minExpectedRoutes = 100

if len(expectedRoutes) < minExpectedRoutes {
t.Errorf("expectedRoutes slice has only %d entries; expected at least %d — did you forget to update?",
len(expectedRoutes), minExpectedRoutes)
}
}
79 changes: 79 additions & 0 deletions pkg/agent/shell_unix_test.go
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
//go:build !windows

package agent

import (
"os/exec"
"strings"
"testing"
)

// TestResolveShell_ReturnsValidPath verifies that resolveShell returns a
// non-empty path to an existing shell binary. On CI/CD environments (Linux)
// at minimum /bin/sh must be available.
func TestResolveShell_ReturnsValidPath(t *testing.T) {
t.Parallel()

path, err := resolveShell()
if err != nil {
t.Fatalf("resolveShell() returned error: %v", err)
}
if path == "" {
t.Fatal("resolveShell() returned empty path")
}

// The returned path should be an absolute path or resolvable via LookPath.
if _, err := exec.LookPath(path); err != nil {
t.Errorf("resolveShell() returned path %q that is not executable: %v", path, err)
}
}

// TestResolveShell_PrefersBash verifies that when bash is available (as it
// is on virtually all CI and dev systems), resolveShell picks it over sh.
func TestResolveShell_PrefersBash(t *testing.T) {
t.Parallel()

// Skip if bash is not installed (e.g., minimal Alpine containers).
if _, err := exec.LookPath("bash"); err != nil {
t.Skip("bash not available on this system")
}

path, err := resolveShell()
if err != nil {
t.Fatalf("resolveShell() returned error: %v", err)
}

if !strings.HasSuffix(path, "/bash") && path != "bash" {
t.Errorf("expected resolveShell() to prefer bash, got %q", path)
}
}

// TestShellFlag_ReturnsMinusC verifies that shellFlag() returns "-c" on Unix.
func TestShellFlag_ReturnsMinusC(t *testing.T) {
t.Parallel()

flag := shellFlag()
if flag != "-c" {
t.Errorf("expected shellFlag() = %q, got %q", "-c", flag)
}
}

// TestIsWindows_ReturnsFalseOnUnix verifies that isWindows() returns false
// when compiled with the !windows build tag.
func TestIsWindows_ReturnsFalseOnUnix(t *testing.T) {
t.Parallel()

if isWindows() {
t.Error("isWindows() returned true on a non-Windows build")
}
}

// TestErrNoShellFound_Message verifies the sentinel error message is stable.
func TestErrNoShellFound_Message(t *testing.T) {
t.Parallel()

expected := "no usable shell found on PATH"
if errNoShellFound.Error() != expected {
t.Errorf("errNoShellFound = %q, want %q", errNoShellFound.Error(), expected)
}
}
Loading