mirror of
https://github.com/rcourtman/Pulse.git
synced 2026-09-24 20:22:53 +00:00
268 lines
12 KiB
Go
268 lines
12 KiB
Go
package mutationregistry
|
|
|
|
import (
|
|
"fmt"
|
|
"os"
|
|
"path/filepath"
|
|
"regexp"
|
|
"sort"
|
|
"strings"
|
|
"testing"
|
|
)
|
|
|
|
type routeClassification struct {
|
|
MutationID string
|
|
}
|
|
|
|
var infrastructureRouteCatalog = map[string]routeClassification{
|
|
"POST /api/actions/plan": {MutationID: "action.api.plan"},
|
|
"POST /api/actions/{id}/refresh": {MutationID: "action.api.refresh"},
|
|
"GET /api/actions/pending": {},
|
|
"GET /api/actions": {},
|
|
"GET /api/actions/{id}": {},
|
|
"POST /api/actions/{id}/decision": {MutationID: "action.api.decision"},
|
|
"POST /api/actions/{id}/execute": {MutationID: "action.api.execute"},
|
|
"POST /api/actions/{id}/force-fail": {MutationID: "action.api.force-fail"},
|
|
"/api/agents/docker/report": {},
|
|
"/api/agents/docker/commands/": {},
|
|
"/api/agents/docker/runtimes/": {MutationID: "docker.api.runtime-stop"},
|
|
"/api/agents/docker/containers/update": {MutationID: "docker.api.update-container"},
|
|
"/api/updates/check": {},
|
|
"/api/updates/apply": {MutationID: "admin.pulse.self-update"},
|
|
"/api/updates/rollback": {MutationID: "admin.pulse.self-update"},
|
|
"/api/updates/status": {},
|
|
"/api/updates/stream": {},
|
|
"/api/updates/plan": {},
|
|
"/api/updates/history": {},
|
|
"/api/updates/history/entry": {},
|
|
"/api/updates/release-notes": {},
|
|
"/api/ai/run-command": {MutationID: "legacy.api.run-command"},
|
|
"/api/ai/remediation/plans": {},
|
|
"/api/ai/remediation/plan": {},
|
|
"/api/ai/remediation/approve": {MutationID: "legacy.enterprise.remediation-approve"},
|
|
"/api/ai/remediation/execute": {MutationID: "legacy.enterprise.remediation-execute"},
|
|
"/api/ai/remediation/rollback": {MutationID: "legacy.enterprise.remediation-rollback"},
|
|
}
|
|
|
|
// TestInfrastructureAPIRoutesResolveToRegistry scans the real router sources,
|
|
// not a duplicate ID inventory. Its scope is deliberately limited to the
|
|
// infrastructure mutation namespaces below; settings/metadata CRUD is outside
|
|
// M1 and is not claimed as infrastructure enumeration.
|
|
func TestInfrastructureAPIRoutesResolveToRegistry(t *testing.T) {
|
|
root := repoRoot(t)
|
|
files := []string{
|
|
"internal/api/router_routes_monitoring.go",
|
|
"internal/api/router_routes_registration.go",
|
|
"internal/api/router_routes_ai_relay.go",
|
|
}
|
|
routeRE := regexp.MustCompile(`"(?:(GET|POST|PUT|DELETE) )?(/api/[^"]+)"`)
|
|
found := map[string]bool{}
|
|
for _, relative := range files {
|
|
data, err := os.ReadFile(filepath.Join(root, relative))
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
for _, match := range routeRE.FindAllStringSubmatch(string(data), -1) {
|
|
method, route := match[1], match[2]
|
|
if !auditedInfrastructurePrefix(route) {
|
|
continue
|
|
}
|
|
key := routeCatalogKey(method, route)
|
|
found[key] = true
|
|
classification, classified := infrastructureRouteCatalog[key]
|
|
if !classified {
|
|
t.Errorf("runtime infrastructure route %q is unclassified", key)
|
|
continue
|
|
}
|
|
if classification.MutationID != "" {
|
|
if err := AuditRuntimeCandidates([]RuntimeCandidate{{Surface: "http:" + key, MutationID: classification.MutationID}}); err != nil {
|
|
t.Error(err)
|
|
}
|
|
}
|
|
}
|
|
}
|
|
for key := range infrastructureRouteCatalog {
|
|
if !found[key] {
|
|
t.Errorf("classified route %q is no longer registered", key)
|
|
}
|
|
}
|
|
// The shared Docker gateway contains a second mutation sub-route.
|
|
dockerSource, _ := os.ReadFile(filepath.Join(root, "internal/api/docker_agents.go"))
|
|
if !strings.Contains(string(dockerSource), `"/update-all"`) {
|
|
t.Fatal("Docker update-all handler candidate not found")
|
|
}
|
|
if err := AuditRuntimeCandidates([]RuntimeCandidate{{Surface: "http:/api/agents/docker/runtimes/#update-all", MutationID: "docker.api.update-all"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func routeCatalogKey(method, route string) string {
|
|
if strings.TrimSpace(method) == "" {
|
|
return route
|
|
}
|
|
return method + " " + route
|
|
}
|
|
|
|
func auditedInfrastructurePrefix(route string) bool {
|
|
if route == "/api/actions" {
|
|
return true
|
|
}
|
|
for _, prefix := range []string{"/api/actions/", "/api/agents/docker/", "/api/updates/", "/api/ai/remediation/", "/api/ai/run-command"} {
|
|
if strings.HasPrefix(route, prefix) {
|
|
return true
|
|
}
|
|
}
|
|
return false
|
|
}
|
|
|
|
func TestActionRouteMethodAuthorityIsExactAndLookalikesFailClosed(t *testing.T) {
|
|
tests := []struct {
|
|
method, route, mutationID string
|
|
classified bool
|
|
}{
|
|
{method: "GET", route: "/api/actions/{id}", classified: true},
|
|
{method: "POST", route: "/api/actions/{id}/execute", mutationID: "action.api.execute", classified: true},
|
|
{method: "DELETE", route: "/api/actions/{id}"},
|
|
{method: "POST", route: "/api/actions/{id}"},
|
|
{method: "GET", route: "/api/actions/{id}/execute"},
|
|
{method: "POST", route: "/api/actions/{id}/execute-copy"},
|
|
}
|
|
for _, tt := range tests {
|
|
key := routeCatalogKey(tt.method, tt.route)
|
|
classification, ok := infrastructureRouteCatalog[key]
|
|
if ok != tt.classified {
|
|
t.Errorf("route %q classified=%v, want %v", key, ok, tt.classified)
|
|
continue
|
|
}
|
|
if ok && classification.MutationID != tt.mutationID {
|
|
t.Errorf("route %q mutation=%q, want %q", key, classification.MutationID, tt.mutationID)
|
|
}
|
|
}
|
|
}
|
|
|
|
type transportClassification struct {
|
|
Role TransportRole
|
|
MutationID string
|
|
DurableAuthorityID string
|
|
}
|
|
|
|
func TestTransportCommandCatalogsResolveToRegistry(t *testing.T) {
|
|
root := repoRoot(t)
|
|
docker := scanNamedStringConstants(t, filepath.Join(root, "internal/monitoring/docker_commands.go"), `DockerCommandType\w+`)
|
|
dockerCatalog := map[string]transportClassification{
|
|
"DockerCommandTypeStop": {Role: TransportRoleMutationRequest, MutationID: "transport.docker.runtime-stop"},
|
|
"DockerCommandTypeUpdateContainer": {Role: TransportRoleMutationRequest, MutationID: "transport.docker.update-container"},
|
|
"DockerCommandTypeUpdateAll": {Role: TransportRoleMutationRequest, MutationID: "transport.docker.update-all"},
|
|
"DockerCommandTypeCheckUpdates": {Role: TransportRoleProtocol},
|
|
}
|
|
auditConstantCatalog(t, docker, dockerCatalog)
|
|
|
|
agent := scanNamedStringConstants(t, filepath.Join(root, "internal/agentexec/types.go"), `MsgType\w+`)
|
|
agentCatalog := map[string]transportClassification{
|
|
"MsgTypeAgentRegister": {Role: TransportRoleProtocol}, "MsgTypeAgentPing": {Role: TransportRoleProtocol},
|
|
"MsgTypeCommandResult": {Role: TransportRoleOperationResult},
|
|
"MsgTypeHostStorageCleanupResult": {Role: TransportRoleOperationResult}, "MsgTypeHostUpdateResult": {Role: TransportRoleOperationResult},
|
|
"MsgTypeDockerContainerLifecycleResult": {Role: TransportRoleOperationResult},
|
|
"MsgTypeDockerContainerUpdateResult": {Role: TransportRoleOperationResult},
|
|
"MsgTypeOperationQueryResult": {Role: TransportRoleOperationReceipt},
|
|
"MsgTypeRegistered": {Role: TransportRoleProtocol}, "MsgTypePong": {Role: TransportRoleProtocol},
|
|
"MsgTypeExecuteCmd": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.raw-command", DurableAuthorityID: "assistant.resource-action"},
|
|
"MsgTypeHostStorageCleanup": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.host-package-cache-cleanup", DurableAuthorityID: "resource.host.package-cache-cleanup"},
|
|
"MsgTypeReadFile": {Role: TransportRoleProtocol},
|
|
"MsgTypeHostUpdate": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.host-package-update", DurableAuthorityID: "resource.host.package-update"},
|
|
"MsgTypeDockerContainerLifecycle": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.docker-container-lifecycle", DurableAuthorityID: "resource.docker.container-lifecycle"},
|
|
"MsgTypeDockerContainerUpdate": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.docker-container-update", DurableAuthorityID: "resource.docker.container-update"},
|
|
"MsgTypeOperationQuery": {Role: TransportRoleOperationQuery},
|
|
"MsgTypeDeployPreflight": {Role: TransportRoleAdministrativeRequest, MutationID: "transport.agent.deploy-preflight"},
|
|
"MsgTypeDeployInstall": {Role: TransportRoleAdministrativeRequest, MutationID: "transport.agent.deploy-install"},
|
|
"MsgTypeDeployCancelJob": {Role: TransportRoleAdministrativeRequest, MutationID: "transport.agent.deploy-cancel"},
|
|
"MsgTypeDeployProgress": {Role: TransportRoleOperationResult},
|
|
}
|
|
auditConstantCatalog(t, agent, agentCatalog)
|
|
}
|
|
|
|
func TestNonAdmittingTransportMessagesCannotCarryDispatchAuthority(t *testing.T) {
|
|
for _, role := range []TransportRole{TransportRoleOperationQuery, TransportRoleOperationResult, TransportRoleOperationReceipt, TransportRoleProtocol} {
|
|
err := AuditTransportSurfaces([]TransportSurface{{
|
|
Name: "lookalike", WireValue: "docker_container_lifecycle_result_retry", Role: role,
|
|
MutationID: "transport.agent.docker-container-lifecycle", DurableAuthorityID: "resource.docker.container-lifecycle",
|
|
}})
|
|
if err == nil {
|
|
t.Errorf("non-admitting role %q accepted dispatch authority", role)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestUnknownTransportLookalikeFailsClosed(t *testing.T) {
|
|
actual := map[string]string{"MsgTypeDockerContainerLifecycle": "docker_container_lifecycle", "MsgTypeDockerContainerLifecycleRetry": "docker_container_lifecycle_retry"}
|
|
catalog := map[string]transportClassification{
|
|
"MsgTypeDockerContainerLifecycle": {Role: TransportRoleMutationRequest, MutationID: "transport.agent.docker-container-lifecycle", DurableAuthorityID: "resource.docker.container-lifecycle"},
|
|
}
|
|
if unknown := unclassifiedTransportConstants(actual, catalog); len(unknown) != 1 || unknown[0] != `MsgTypeDockerContainerLifecycleRetry="docker_container_lifecycle_retry"` {
|
|
t.Fatalf("unknown lookalike classification = %v", unknown)
|
|
}
|
|
}
|
|
|
|
func TestPatrolJobRegistrationResolvesToRegistry(t *testing.T) {
|
|
root := repoRoot(t)
|
|
router, _ := os.ReadFile(filepath.Join(root, "internal/api/router.go"))
|
|
broker, _ := os.ReadFile(filepath.Join(root, "internal/api/patrol_action_broker.go"))
|
|
if !strings.Contains(string(router), "SetActionBrokerFactory") || !strings.Contains(string(broker), "PlanWithOptions") {
|
|
t.Fatal("Patrol action job registration is no longer mechanically discoverable")
|
|
}
|
|
if err := AuditRuntimeCandidates([]RuntimeCandidate{{Surface: "job:patrol-action-broker", MutationID: "patrol.typed-action"}}); err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
}
|
|
|
|
func scanNamedStringConstants(t *testing.T, path, namePattern string) map[string]string {
|
|
t.Helper()
|
|
data, err := os.ReadFile(path)
|
|
if err != nil {
|
|
t.Fatal(err)
|
|
}
|
|
re := regexp.MustCompile(`(?m)^\s*(` + namePattern + `)(?:\s+\w+)?\s*=\s*"([^"]+)"`)
|
|
out := map[string]string{}
|
|
for _, match := range re.FindAllStringSubmatch(string(data), -1) {
|
|
out[match[1]] = match[2]
|
|
}
|
|
return out
|
|
}
|
|
|
|
func auditConstantCatalog(t *testing.T, actual map[string]string, catalog map[string]transportClassification) {
|
|
t.Helper()
|
|
var surfaces []TransportSurface
|
|
for name, value := range actual {
|
|
classification, ok := catalog[name]
|
|
if !ok {
|
|
t.Errorf("transport command %s=%q is unclassified", name, value)
|
|
continue
|
|
}
|
|
surfaces = append(surfaces, TransportSurface{Name: name, WireValue: value, Role: classification.Role, MutationID: classification.MutationID, DurableAuthorityID: classification.DurableAuthorityID})
|
|
}
|
|
if err := AuditTransportSurfaces(surfaces); err != nil {
|
|
t.Error(err)
|
|
}
|
|
var stale []string
|
|
for name := range catalog {
|
|
if _, ok := actual[name]; !ok {
|
|
stale = append(stale, name)
|
|
}
|
|
}
|
|
sort.Strings(stale)
|
|
if len(stale) > 0 {
|
|
t.Errorf("classified transport constants no longer exist: %v", stale)
|
|
}
|
|
}
|
|
|
|
func unclassifiedTransportConstants(actual map[string]string, catalog map[string]transportClassification) []string {
|
|
var unknown []string
|
|
for name, value := range actual {
|
|
if _, ok := catalog[name]; !ok {
|
|
unknown = append(unknown, fmt.Sprintf(`%s=%q`, name, value))
|
|
}
|
|
}
|
|
sort.Strings(unknown)
|
|
return unknown
|
|
}
|