package agent import ( "context" "testing" mcpv1 "git.wntrmute.dev/kyle/mcp/gen/mcp/v1" "git.wntrmute.dev/kyle/mcp/internal/registry" "git.wntrmute.dev/kyle/mcp/internal/runtime" ) func TestListServices(t *testing.T) { a := newTestAgent(t, &fakeRuntime{}) ctx := context.Background() // Empty registry. resp, err := a.ListServices(ctx, &mcpv1.ListServicesRequest{}) if err != nil { t.Fatalf("ListServices: %v", err) } if len(resp.Services) != 0 { t.Fatalf("expected 0 services, got %d", len(resp.Services)) } // Add a service with components. if err := registry.CreateService(a.DB, "metacrypt", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "api", Service: "metacrypt", Image: "img:v1", DesiredState: "running", ObservedState: "running", Version: "v1", }); err != nil { t.Fatalf("create component: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "web", Service: "metacrypt", Image: "img-web:v1", DesiredState: "running", ObservedState: "unknown", Version: "v1", }); err != nil { t.Fatalf("create component: %v", err) } resp, err = a.ListServices(ctx, &mcpv1.ListServicesRequest{}) if err != nil { t.Fatalf("ListServices: %v", err) } if len(resp.Services) != 1 { t.Fatalf("expected 1 service, got %d", len(resp.Services)) } svc := resp.Services[0] if svc.Name != "metacrypt" || !svc.Active { t.Fatalf("unexpected service: %+v", svc) } if len(svc.Components) != 2 { t.Fatalf("expected 2 components, got %d", len(svc.Components)) } if svc.Components[0].Name != "api" { t.Fatalf("expected first component 'api', got %q", svc.Components[0].Name) } } func TestLiveCheck(t *testing.T) { rt := &fakeRuntime{ containers: []runtime.ContainerInfo{ {Name: "metacrypt-api", Image: "img:v1", State: "running", Version: "v1"}, {Name: "unmanaged-thing", Image: "other:latest", State: "running", Version: "latest"}, }, } a := newTestAgent(t, rt) ctx := context.Background() // Set up registry with one service and one component. if err := registry.CreateService(a.DB, "metacrypt", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "api", Service: "metacrypt", Image: "img:v1", DesiredState: "running", ObservedState: "unknown", Version: "v1", }); err != nil { t.Fatalf("create component: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "web", Service: "metacrypt", Image: "img-web:v1", DesiredState: "running", ObservedState: "unknown", Version: "v1", }); err != nil { t.Fatalf("create component: %v", err) } resp, err := a.LiveCheck(ctx, &mcpv1.LiveCheckRequest{}) if err != nil { t.Fatalf("LiveCheck: %v", err) } // Should have 2 entries: the registered service and the unmanaged container. if len(resp.Services) != 2 { t.Fatalf("expected 2 service entries, got %d", len(resp.Services)) } // First entry: metacrypt (registered). mc := resp.Services[0] if mc.Name != "metacrypt" { t.Fatalf("expected 'metacrypt', got %q", mc.Name) } if len(mc.Components) != 2 { t.Fatalf("expected 2 components, got %d", len(mc.Components)) } // api should be running (found in runtime). api := mc.Components[0] if api.Name != "api" || api.ObservedState != "running" { t.Fatalf("api: expected observed_state=running, got %q", api.ObservedState) } // web should be removed (not found in runtime). web := mc.Components[1] if web.Name != "web" || web.ObservedState != "removed" { t.Fatalf("web: expected observed_state=removed, got %q", web.ObservedState) } // Second entry: unmanaged container. unmanaged := resp.Services[1] if unmanaged.Name != "unmanaged" { t.Fatalf("expected unmanaged service name 'unmanaged', got %q", unmanaged.Name) } if len(unmanaged.Components) != 1 { t.Fatalf("expected 1 unmanaged component, got %d", len(unmanaged.Components)) } uc := unmanaged.Components[0] if uc.DesiredState != "ignore" { t.Fatalf("unmanaged desired_state: expected 'ignore', got %q", uc.DesiredState) } if uc.ObservedState != "running" { t.Fatalf("unmanaged observed_state: expected 'running', got %q", uc.ObservedState) } } func TestGetServiceStatus_DriftDetection(t *testing.T) { rt := &fakeRuntime{ containers: []runtime.ContainerInfo{ {Name: "metacrypt-api", Image: "img:v1", State: "exited", Version: "v1"}, }, } a := newTestAgent(t, rt) ctx := context.Background() if err := registry.CreateService(a.DB, "metacrypt", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "api", Service: "metacrypt", Image: "img:v1", DesiredState: "running", ObservedState: "running", Version: "v1", }); err != nil { t.Fatalf("create component: %v", err) } // Add an event so we can verify it appears. if err := registry.InsertEvent(a.DB, "metacrypt", "api", "running", "exited"); err != nil { t.Fatalf("insert event: %v", err) } resp, err := a.GetServiceStatus(ctx, &mcpv1.GetServiceStatusRequest{Name: "metacrypt"}) if err != nil { t.Fatalf("GetServiceStatus: %v", err) } if len(resp.Services) != 1 { t.Fatalf("expected 1 service, got %d", len(resp.Services)) } if resp.Services[0].Name != "metacrypt" { t.Fatalf("expected 'metacrypt', got %q", resp.Services[0].Name) } // Drift: desired=running, observed=exited. if len(resp.Drift) != 1 { t.Fatalf("expected 1 drift entry, got %d", len(resp.Drift)) } d := resp.Drift[0] if d.Service != "metacrypt" || d.Component != "api" { t.Fatalf("drift: unexpected service/component: %q/%q", d.Service, d.Component) } if d.DesiredState != "running" || d.ObservedState != "exited" { t.Fatalf("drift: desired=%q, observed=%q", d.DesiredState, d.ObservedState) } // Events. if len(resp.RecentEvents) != 1 { t.Fatalf("expected 1 event, got %d", len(resp.RecentEvents)) } ev := resp.RecentEvents[0] if ev.PrevState != "running" || ev.NewState != "exited" { t.Fatalf("event: prev=%q, new=%q", ev.PrevState, ev.NewState) } } func TestGetServiceStatus_FilterByName(t *testing.T) { rt := &fakeRuntime{ containers: []runtime.ContainerInfo{ {Name: "metacrypt-api", Image: "img:v1", State: "running", Version: "v1"}, {Name: "mcr-api", Image: "mcr:v1", State: "running", Version: "v1"}, }, } a := newTestAgent(t, rt) ctx := context.Background() for _, svc := range []string{"metacrypt", "mcr"} { if err := registry.CreateService(a.DB, svc, true); err != nil { t.Fatalf("create service %q: %v", svc, err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "api", Service: svc, Image: svc + ":v1", DesiredState: "running", ObservedState: "unknown", Version: "v1", }); err != nil { t.Fatalf("create component for %q: %v", svc, err) } } resp, err := a.GetServiceStatus(ctx, &mcpv1.GetServiceStatusRequest{Name: "mcr"}) if err != nil { t.Fatalf("GetServiceStatus: %v", err) } if len(resp.Services) != 1 { t.Fatalf("expected 1 service, got %d", len(resp.Services)) } if resp.Services[0].Name != "mcr" { t.Fatalf("expected 'mcr', got %q", resp.Services[0].Name) } } func TestGetServiceStatus_IgnoreSkipsDrift(t *testing.T) { rt := &fakeRuntime{ containers: []runtime.ContainerInfo{ {Name: "random-thing", Image: "img:v1", State: "running", Version: "v1"}, }, } a := newTestAgent(t, rt) ctx := context.Background() // No registered services, only an unmanaged container. resp, err := a.GetServiceStatus(ctx, &mcpv1.GetServiceStatusRequest{}) if err != nil { t.Fatalf("GetServiceStatus: %v", err) } // The unmanaged container should not appear in drift. if len(resp.Drift) != 0 { t.Fatalf("expected 0 drift entries for unmanaged, got %d", len(resp.Drift)) } } func TestSplitContainerName(t *testing.T) { tests := []struct { name string service string comp string }{ {"metacrypt-api", "metacrypt", "api"}, {"metacrypt-web-ui", "metacrypt", "web-ui"}, {"standalone", "standalone", "standalone"}, } for _, tt := range tests { t.Run(tt.name, func(t *testing.T) { svc, comp := splitContainerName(tt.name) if svc != tt.service || comp != tt.comp { t.Fatalf("splitContainerName(%q) = (%q, %q), want (%q, %q)", tt.name, svc, comp, tt.service, tt.comp) } }) } }