package agent import ( "context" "testing" mcpv1 "git.wntrmute.dev/mc/mcp/gen/mcp/v1" "git.wntrmute.dev/mc/mcp/internal/registry" ) func TestPurgeComponentRemoved(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() // Set up a service with a stale component. if err := registry.CreateService(a.DB, "mcns", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "coredns", Service: "mcns", Image: "coredns:latest", DesiredState: "running", ObservedState: "removed", }); err != nil { t.Fatalf("create component: %v", err) } // Insert an event for this component. if err := registry.InsertEvent(a.DB, "mcns", "coredns", "running", "removed"); err != nil { t.Fatalf("insert event: %v", err) } resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{ DefinedComponents: []string{"mcns/mcns"}, }) if err != nil { t.Fatalf("PurgeComponent: %v", err) } if len(resp.Results) != 1 { t.Fatalf("expected 1 result, got %d", len(resp.Results)) } r := resp.Results[0] if !r.Purged { t.Fatalf("expected purged=true, got reason: %s", r.Reason) } if r.Service != "mcns" || r.Component != "coredns" { t.Fatalf("unexpected result: %s/%s", r.Service, r.Component) } // Verify component was deleted. _, err = registry.GetComponent(a.DB, "mcns", "coredns") if err == nil { t.Fatal("component should have been deleted") } // Service should also be deleted since it has no remaining components. _, err = registry.GetService(a.DB, "mcns") if err == nil { t.Fatal("service should have been deleted (no remaining components)") } } func TestPurgeRefusesRunning(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() if err := registry.CreateService(a.DB, "mcr", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "api", Service: "mcr", Image: "mcr:latest", DesiredState: "running", ObservedState: "running", }); err != nil { t.Fatalf("create component: %v", err) } resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{ Service: "mcr", Component: "api", }) if err != nil { t.Fatalf("PurgeComponent: %v", err) } if len(resp.Results) != 1 { t.Fatalf("expected 1 result, got %d", len(resp.Results)) } if resp.Results[0].Purged { t.Fatal("should not purge a running component") } // Verify component still exists. _, err = registry.GetComponent(a.DB, "mcr", "api") if err != nil { t.Fatalf("component should still exist: %v", err) } } func TestPurgeRefusesStopped(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() if err := registry.CreateService(a.DB, "mcr", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "api", Service: "mcr", Image: "mcr:latest", DesiredState: "stopped", ObservedState: "stopped", }); err != nil { t.Fatalf("create component: %v", err) } resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{ Service: "mcr", Component: "api", }) if err != nil { t.Fatalf("PurgeComponent: %v", err) } if resp.Results[0].Purged { t.Fatal("should not purge a stopped component") } } func TestPurgeSkipsDefinedComponent(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() if err := registry.CreateService(a.DB, "mcns", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "mcns", Service: "mcns", Image: "mcns:latest", DesiredState: "running", ObservedState: "exited", }); err != nil { t.Fatalf("create component: %v", err) } resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{ DefinedComponents: []string{"mcns/mcns"}, }) if err != nil { t.Fatalf("PurgeComponent: %v", err) } if len(resp.Results) != 1 { t.Fatalf("expected 1 result, got %d", len(resp.Results)) } if resp.Results[0].Purged { t.Fatal("should not purge a component that is still in service definitions") } if resp.Results[0].Reason != "still in service definitions" { t.Fatalf("unexpected reason: %s", resp.Results[0].Reason) } } func TestPurgeDryRun(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() if err := registry.CreateService(a.DB, "mcns", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "coredns", Service: "mcns", Image: "coredns:latest", DesiredState: "running", ObservedState: "removed", }); err != nil { t.Fatalf("create component: %v", err) } resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{ DryRun: true, DefinedComponents: []string{"mcns/mcns"}, }) if err != nil { t.Fatalf("PurgeComponent: %v", err) } if len(resp.Results) != 1 { t.Fatalf("expected 1 result, got %d", len(resp.Results)) } if !resp.Results[0].Purged { t.Fatal("dry run should report purged=true for eligible components") } // Verify component was NOT deleted (dry run). _, err = registry.GetComponent(a.DB, "mcns", "coredns") if err != nil { t.Fatalf("component should still exist after dry run: %v", err) } } func TestPurgeServiceFilter(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() // Create two services. if err := registry.CreateService(a.DB, "mcns", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "coredns", Service: "mcns", Image: "coredns:latest", DesiredState: "running", ObservedState: "removed", }); err != nil { t.Fatalf("create component: %v", err) } if err := registry.CreateService(a.DB, "mcr", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "old", Service: "mcr", Image: "old:latest", DesiredState: "running", ObservedState: "removed", }); err != nil { t.Fatalf("create component: %v", err) } // Purge only mcns. resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{ Service: "mcns", }) if err != nil { t.Fatalf("PurgeComponent: %v", err) } if len(resp.Results) != 1 { t.Fatalf("expected 1 result, got %d", len(resp.Results)) } if resp.Results[0].Service != "mcns" { t.Fatalf("expected mcns, got %s", resp.Results[0].Service) } // mcr/old should still exist. _, err = registry.GetComponent(a.DB, "mcr", "old") if err != nil { t.Fatalf("mcr/old should still exist: %v", err) } } func TestPurgeServiceDeletedWhenEmpty(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() if err := registry.CreateService(a.DB, "mcns", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "coredns", Service: "mcns", Image: "coredns:latest", DesiredState: "running", ObservedState: "removed", }); err != nil { t.Fatalf("create component: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "old-thing", Service: "mcns", Image: "old:latest", DesiredState: "stopped", ObservedState: "unknown", }); err != nil { t.Fatalf("create component: %v", err) } resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{}) if err != nil { t.Fatalf("PurgeComponent: %v", err) } // Both components should be purged. if len(resp.Results) != 2 { t.Fatalf("expected 2 results, got %d", len(resp.Results)) } for _, r := range resp.Results { if !r.Purged { t.Fatalf("expected purged=true for %s/%s: %s", r.Service, r.Component, r.Reason) } } // Service should be deleted. _, err = registry.GetService(a.DB, "mcns") if err == nil { t.Fatal("service should have been deleted") } } func TestPurgeServiceKeptWhenComponentsRemain(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() if err := registry.CreateService(a.DB, "mcns", true); err != nil { t.Fatalf("create service: %v", err) } // Stale component (will be purged). if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "coredns", Service: "mcns", Image: "coredns:latest", DesiredState: "running", ObservedState: "removed", }); err != nil { t.Fatalf("create component: %v", err) } // Live component (will not be purged). if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "mcns", Service: "mcns", Image: "mcns:latest", DesiredState: "running", ObservedState: "running", }); err != nil { t.Fatalf("create component: %v", err) } resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{}) if err != nil { t.Fatalf("PurgeComponent: %v", err) } if len(resp.Results) != 2 { t.Fatalf("expected 2 results, got %d", len(resp.Results)) } // coredns should be purged, mcns should not. purged := 0 for _, r := range resp.Results { if r.Purged { purged++ if r.Component != "coredns" { t.Fatalf("expected coredns to be purged, got %s", r.Component) } } } if purged != 1 { t.Fatalf("expected 1 purged, got %d", purged) } // Service should still exist. _, err = registry.GetService(a.DB, "mcns") if err != nil { t.Fatalf("service should still exist: %v", err) } } func TestPurgeExitedState(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() if err := registry.CreateService(a.DB, "test", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "old", Service: "test", Image: "old:latest", DesiredState: "stopped", ObservedState: "exited", }); err != nil { t.Fatalf("create component: %v", err) } resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{}) if err != nil { t.Fatalf("PurgeComponent: %v", err) } if len(resp.Results) != 1 || !resp.Results[0].Purged { t.Fatalf("exited component should be purgeable") } } func TestPurgeUnknownState(t *testing.T) { rt := &fakeRuntime{} a := newTestAgent(t, rt) ctx := context.Background() if err := registry.CreateService(a.DB, "test", true); err != nil { t.Fatalf("create service: %v", err) } if err := registry.CreateComponent(a.DB, ®istry.Component{ Name: "ghost", Service: "test", Image: "ghost:latest", DesiredState: "running", ObservedState: "unknown", }); err != nil { t.Fatalf("create component: %v", err) } resp, err := a.PurgeComponent(ctx, &mcpv1.PurgeRequest{}) if err != nil { t.Fatalf("PurgeComponent: %v", err) } if len(resp.Results) != 1 || !resp.Results[0].Purged { t.Fatalf("unknown component should be purgeable") } }