New internal/masterdb/ package for mcp-master cluster state. Separate from the agent's registry because the schemas are fundamentally different (cluster-wide placement vs node-local containers). Tables: nodes, placements, edge_routes. Full CRUD with tests. Follows the same Open/migrate pattern as internal/registry/. Co-Authored-By: Claude Opus 4.6 (1M context) <noreply@anthropic.com>
186 lines
4.6 KiB
Go
186 lines
4.6 KiB
Go
package masterdb
|
|
|
|
import (
|
|
"database/sql"
|
|
"path/filepath"
|
|
"testing"
|
|
)
|
|
|
|
func openTestDB(t *testing.T) *sql.DB {
|
|
t.Helper()
|
|
path := filepath.Join(t.TempDir(), "test.db")
|
|
db, err := Open(path)
|
|
if err != nil {
|
|
t.Fatalf("Open: %v", err)
|
|
}
|
|
t.Cleanup(func() { _ = db.Close() })
|
|
return db
|
|
}
|
|
|
|
func TestOpenAndMigrate(t *testing.T) {
|
|
openTestDB(t)
|
|
}
|
|
|
|
func TestNodeCRUD(t *testing.T) {
|
|
db := openTestDB(t)
|
|
|
|
if err := UpsertNode(db, "rift", "100.95.252.120:9444", "master", "amd64"); err != nil {
|
|
t.Fatalf("UpsertNode: %v", err)
|
|
}
|
|
if err := UpsertNode(db, "svc", "100.106.232.4:9555", "edge", "amd64"); err != nil {
|
|
t.Fatalf("UpsertNode: %v", err)
|
|
}
|
|
if err := UpsertNode(db, "orion", "100.1.2.3:9444", "worker", "amd64"); err != nil {
|
|
t.Fatalf("UpsertNode: %v", err)
|
|
}
|
|
|
|
// Get.
|
|
n, err := GetNode(db, "rift")
|
|
if err != nil {
|
|
t.Fatalf("GetNode: %v", err)
|
|
}
|
|
if n == nil || n.Address != "100.95.252.120:9444" {
|
|
t.Errorf("GetNode(rift) = %+v", n)
|
|
}
|
|
|
|
// Get nonexistent.
|
|
n, err = GetNode(db, "nonexistent")
|
|
if err != nil {
|
|
t.Fatalf("GetNode: %v", err)
|
|
}
|
|
if n != nil {
|
|
t.Errorf("expected nil for nonexistent node")
|
|
}
|
|
|
|
// List all.
|
|
nodes, err := ListNodes(db)
|
|
if err != nil {
|
|
t.Fatalf("ListNodes: %v", err)
|
|
}
|
|
if len(nodes) != 3 {
|
|
t.Errorf("ListNodes: got %d, want 3", len(nodes))
|
|
}
|
|
|
|
// List workers (includes master role).
|
|
workers, err := ListWorkerNodes(db)
|
|
if err != nil {
|
|
t.Fatalf("ListWorkerNodes: %v", err)
|
|
}
|
|
if len(workers) != 2 {
|
|
t.Errorf("ListWorkerNodes: got %d, want 2 (rift+orion)", len(workers))
|
|
}
|
|
|
|
// List edge.
|
|
edges, err := ListEdgeNodes(db)
|
|
if err != nil {
|
|
t.Fatalf("ListEdgeNodes: %v", err)
|
|
}
|
|
if len(edges) != 1 || edges[0].Name != "svc" {
|
|
t.Errorf("ListEdgeNodes: got %v", edges)
|
|
}
|
|
|
|
// Update status.
|
|
if err := UpdateNodeStatus(db, "rift", "healthy"); err != nil {
|
|
t.Fatalf("UpdateNodeStatus: %v", err)
|
|
}
|
|
n, _ = GetNode(db, "rift")
|
|
if n.Status != "healthy" {
|
|
t.Errorf("status = %q, want healthy", n.Status)
|
|
}
|
|
}
|
|
|
|
func TestPlacementCRUD(t *testing.T) {
|
|
db := openTestDB(t)
|
|
_ = UpsertNode(db, "rift", "100.95.252.120:9444", "master", "amd64")
|
|
_ = UpsertNode(db, "orion", "100.1.2.3:9444", "worker", "amd64")
|
|
|
|
if err := CreatePlacement(db, "mcq", "rift", "worker"); err != nil {
|
|
t.Fatalf("CreatePlacement: %v", err)
|
|
}
|
|
if err := CreatePlacement(db, "mcdoc", "orion", "worker"); err != nil {
|
|
t.Fatalf("CreatePlacement: %v", err)
|
|
}
|
|
|
|
p, err := GetPlacement(db, "mcq")
|
|
if err != nil {
|
|
t.Fatalf("GetPlacement: %v", err)
|
|
}
|
|
if p == nil || p.Node != "rift" {
|
|
t.Errorf("GetPlacement(mcq) = %+v", p)
|
|
}
|
|
|
|
p, _ = GetPlacement(db, "nonexistent")
|
|
if p != nil {
|
|
t.Errorf("expected nil for nonexistent placement")
|
|
}
|
|
|
|
counts, err := CountPlacementsPerNode(db)
|
|
if err != nil {
|
|
t.Fatalf("CountPlacementsPerNode: %v", err)
|
|
}
|
|
if counts["rift"] != 1 || counts["orion"] != 1 {
|
|
t.Errorf("counts = %v", counts)
|
|
}
|
|
|
|
placements, err := ListPlacements(db)
|
|
if err != nil {
|
|
t.Fatalf("ListPlacements: %v", err)
|
|
}
|
|
if len(placements) != 2 {
|
|
t.Errorf("ListPlacements: got %d", len(placements))
|
|
}
|
|
|
|
if err := DeletePlacement(db, "mcq"); err != nil {
|
|
t.Fatalf("DeletePlacement: %v", err)
|
|
}
|
|
p, _ = GetPlacement(db, "mcq")
|
|
if p != nil {
|
|
t.Errorf("expected nil after delete")
|
|
}
|
|
}
|
|
|
|
func TestEdgeRouteCRUD(t *testing.T) {
|
|
db := openTestDB(t)
|
|
_ = UpsertNode(db, "svc", "100.106.232.4:9555", "edge", "amd64")
|
|
|
|
if err := CreateEdgeRoute(db, "mcq.metacircular.net", "mcq", "svc", "mcq.svc.mcp.metacircular.net", 8443); err != nil {
|
|
t.Fatalf("CreateEdgeRoute: %v", err)
|
|
}
|
|
if err := CreateEdgeRoute(db, "docs.metacircular.net", "mcdoc", "svc", "mcdoc.svc.mcp.metacircular.net", 443); err != nil {
|
|
t.Fatalf("CreateEdgeRoute: %v", err)
|
|
}
|
|
|
|
routes, err := ListEdgeRoutes(db)
|
|
if err != nil {
|
|
t.Fatalf("ListEdgeRoutes: %v", err)
|
|
}
|
|
if len(routes) != 2 {
|
|
t.Errorf("ListEdgeRoutes: got %d", len(routes))
|
|
}
|
|
|
|
routes, err = ListEdgeRoutesForService(db, "mcq")
|
|
if err != nil {
|
|
t.Fatalf("ListEdgeRoutesForService: %v", err)
|
|
}
|
|
if len(routes) != 1 || routes[0].Hostname != "mcq.metacircular.net" {
|
|
t.Errorf("ListEdgeRoutesForService(mcq) = %v", routes)
|
|
}
|
|
|
|
if err := DeleteEdgeRoute(db, "mcq.metacircular.net"); err != nil {
|
|
t.Fatalf("DeleteEdgeRoute: %v", err)
|
|
}
|
|
routes, _ = ListEdgeRoutes(db)
|
|
if len(routes) != 1 {
|
|
t.Errorf("expected 1 route after delete, got %d", len(routes))
|
|
}
|
|
|
|
_ = CreateEdgeRoute(db, "docs2.metacircular.net", "mcdoc", "svc", "mcdoc.svc.mcp.metacircular.net", 443)
|
|
if err := DeleteEdgeRoutesForService(db, "mcdoc"); err != nil {
|
|
t.Fatalf("DeleteEdgeRoutesForService: %v", err)
|
|
}
|
|
routes, _ = ListEdgeRoutes(db)
|
|
if len(routes) != 0 {
|
|
t.Errorf("expected 0 routes after service delete, got %d", len(routes))
|
|
}
|
|
}
|