Add modules.Registry and config.Config fields to Server struct. Create registry in main.go, load state from YAML+DB, log all module states at startup. Conditionally start job/runner sweeper goroutines only when the jobs module is enabled. Update all 5 test files to pass registry to NewServer. Ref #95, #96
250 lines
6.9 KiB
Go
250 lines
6.9 KiB
Go
package api
|
|
|
|
import (
|
|
"context"
|
|
"encoding/json"
|
|
"net/http"
|
|
"net/http/httptest"
|
|
"strings"
|
|
"testing"
|
|
|
|
"github.com/go-chi/chi/v5"
|
|
"github.com/kindredsystems/silo/internal/db"
|
|
"github.com/kindredsystems/silo/internal/modules"
|
|
"github.com/kindredsystems/silo/internal/schema"
|
|
"github.com/kindredsystems/silo/internal/testutil"
|
|
"github.com/rs/zerolog"
|
|
)
|
|
|
|
func newDAGTestServer(t *testing.T) *Server {
|
|
t.Helper()
|
|
pool := testutil.MustConnectTestPool(t)
|
|
database := db.NewFromPool(pool)
|
|
broker := NewBroker(zerolog.Nop())
|
|
state := NewServerState(zerolog.Nop(), nil, broker)
|
|
return NewServer(
|
|
zerolog.Nop(),
|
|
database,
|
|
map[string]*schema.Schema{},
|
|
"",
|
|
nil, nil, nil, nil, nil,
|
|
broker, state,
|
|
nil, "",
|
|
modules.NewRegistry(), nil,
|
|
)
|
|
}
|
|
|
|
func newDAGRouter(s *Server) http.Handler {
|
|
r := chi.NewRouter()
|
|
r.Route("/api/items/{partNumber}", func(r chi.Router) {
|
|
r.Get("/dag", s.HandleGetDAG)
|
|
r.Get("/dag/forward-cone/{nodeKey}", s.HandleGetForwardCone)
|
|
r.Get("/dag/dirty", s.HandleGetDirtySubgraph)
|
|
r.Put("/dag", s.HandleSyncDAG)
|
|
r.Post("/dag/mark-dirty/{nodeKey}", s.HandleMarkDirty)
|
|
})
|
|
return r
|
|
}
|
|
|
|
func TestHandleGetDAG_Empty(t *testing.T) {
|
|
s := newDAGTestServer(t)
|
|
r := newDAGRouter(s)
|
|
|
|
// Create an item
|
|
item := &db.Item{PartNumber: "DAG-TEST-001", ItemType: "part", Description: "DAG test"}
|
|
if err := s.items.Create(context.Background(), item, nil); err != nil {
|
|
t.Fatalf("creating item: %v", err)
|
|
}
|
|
|
|
req := httptest.NewRequest("GET", "/api/items/DAG-TEST-001/dag", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if resp["part_number"] != "DAG-TEST-001" {
|
|
t.Errorf("expected part_number DAG-TEST-001, got %v", resp["part_number"])
|
|
}
|
|
}
|
|
|
|
func TestHandleSyncDAG(t *testing.T) {
|
|
s := newDAGTestServer(t)
|
|
r := newDAGRouter(s)
|
|
|
|
// Create an item with a revision
|
|
item := &db.Item{PartNumber: "DAG-SYNC-001", ItemType: "part", Description: "sync test"}
|
|
if err := s.items.Create(context.Background(), item, nil); err != nil {
|
|
t.Fatalf("creating item: %v", err)
|
|
}
|
|
|
|
// Sync a feature tree
|
|
body := `{
|
|
"nodes": [
|
|
{"node_key": "Sketch001", "node_type": "sketch"},
|
|
{"node_key": "Pad001", "node_type": "pad"},
|
|
{"node_key": "Fillet001", "node_type": "fillet"}
|
|
],
|
|
"edges": [
|
|
{"source_key": "Sketch001", "target_key": "Pad001", "edge_type": "depends_on"},
|
|
{"source_key": "Pad001", "target_key": "Fillet001", "edge_type": "depends_on"}
|
|
]
|
|
}`
|
|
|
|
req := httptest.NewRequest("PUT", "/api/items/DAG-SYNC-001/dag", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("expected 200, got %d: %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(w.Body.Bytes(), &resp)
|
|
if resp["node_count"] != float64(3) {
|
|
t.Errorf("expected 3 nodes, got %v", resp["node_count"])
|
|
}
|
|
if resp["edge_count"] != float64(2) {
|
|
t.Errorf("expected 2 edges, got %v", resp["edge_count"])
|
|
}
|
|
|
|
// Verify we can read the DAG back
|
|
req2 := httptest.NewRequest("GET", "/api/items/DAG-SYNC-001/dag", nil)
|
|
w2 := httptest.NewRecorder()
|
|
r.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != http.StatusOK {
|
|
t.Fatalf("GET dag: expected 200, got %d", w2.Code)
|
|
}
|
|
var dagResp map[string]any
|
|
json.Unmarshal(w2.Body.Bytes(), &dagResp)
|
|
nodes, ok := dagResp["nodes"].([]any)
|
|
if !ok || len(nodes) != 3 {
|
|
t.Errorf("expected 3 nodes in GET, got %v", dagResp["nodes"])
|
|
}
|
|
}
|
|
|
|
func TestHandleForwardCone(t *testing.T) {
|
|
s := newDAGTestServer(t)
|
|
r := newDAGRouter(s)
|
|
|
|
item := &db.Item{PartNumber: "DAG-CONE-001", ItemType: "part", Description: "cone test"}
|
|
if err := s.items.Create(context.Background(), item, nil); err != nil {
|
|
t.Fatalf("creating item: %v", err)
|
|
}
|
|
|
|
// Sync a linear chain: A -> B -> C
|
|
body := `{
|
|
"nodes": [
|
|
{"node_key": "A", "node_type": "sketch"},
|
|
{"node_key": "B", "node_type": "pad"},
|
|
{"node_key": "C", "node_type": "fillet"}
|
|
],
|
|
"edges": [
|
|
{"source_key": "A", "target_key": "B"},
|
|
{"source_key": "B", "target_key": "C"}
|
|
]
|
|
}`
|
|
req := httptest.NewRequest("PUT", "/api/items/DAG-CONE-001/dag", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("sync: %d %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Forward cone from A should include B and C
|
|
req2 := httptest.NewRequest("GET", "/api/items/DAG-CONE-001/dag/forward-cone/A", nil)
|
|
w2 := httptest.NewRecorder()
|
|
r.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != http.StatusOK {
|
|
t.Fatalf("forward-cone: %d %s", w2.Code, w2.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(w2.Body.Bytes(), &resp)
|
|
cone, ok := resp["cone"].([]any)
|
|
if !ok || len(cone) != 2 {
|
|
t.Errorf("expected 2 nodes in forward cone, got %v", resp["cone"])
|
|
}
|
|
}
|
|
|
|
func TestHandleMarkDirty(t *testing.T) {
|
|
s := newDAGTestServer(t)
|
|
r := newDAGRouter(s)
|
|
|
|
item := &db.Item{PartNumber: "DAG-DIRTY-001", ItemType: "part", Description: "dirty test"}
|
|
if err := s.items.Create(context.Background(), item, nil); err != nil {
|
|
t.Fatalf("creating item: %v", err)
|
|
}
|
|
|
|
// Sync: A -> B -> C
|
|
body := `{
|
|
"nodes": [
|
|
{"node_key": "X", "node_type": "sketch"},
|
|
{"node_key": "Y", "node_type": "pad"},
|
|
{"node_key": "Z", "node_type": "fillet"}
|
|
],
|
|
"edges": [
|
|
{"source_key": "X", "target_key": "Y"},
|
|
{"source_key": "Y", "target_key": "Z"}
|
|
]
|
|
}`
|
|
req := httptest.NewRequest("PUT", "/api/items/DAG-DIRTY-001/dag", strings.NewReader(body))
|
|
req.Header.Set("Content-Type", "application/json")
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
if w.Code != http.StatusOK {
|
|
t.Fatalf("sync: %d %s", w.Code, w.Body.String())
|
|
}
|
|
|
|
// Mark X dirty — should propagate to Y and Z
|
|
req2 := httptest.NewRequest("POST", "/api/items/DAG-DIRTY-001/dag/mark-dirty/X", nil)
|
|
w2 := httptest.NewRecorder()
|
|
r.ServeHTTP(w2, req2)
|
|
|
|
if w2.Code != http.StatusOK {
|
|
t.Fatalf("mark-dirty: %d %s", w2.Code, w2.Body.String())
|
|
}
|
|
|
|
var resp map[string]any
|
|
json.Unmarshal(w2.Body.Bytes(), &resp)
|
|
affected := resp["nodes_affected"].(float64)
|
|
if affected != 3 {
|
|
t.Errorf("expected 3 nodes affected, got %v", affected)
|
|
}
|
|
|
|
// Verify dirty subgraph
|
|
req3 := httptest.NewRequest("GET", "/api/items/DAG-DIRTY-001/dag/dirty", nil)
|
|
w3 := httptest.NewRecorder()
|
|
r.ServeHTTP(w3, req3)
|
|
|
|
if w3.Code != http.StatusOK {
|
|
t.Fatalf("dirty: %d %s", w3.Code, w3.Body.String())
|
|
}
|
|
var dirtyResp map[string]any
|
|
json.Unmarshal(w3.Body.Bytes(), &dirtyResp)
|
|
dirtyNodes, ok := dirtyResp["nodes"].([]any)
|
|
if !ok || len(dirtyNodes) != 3 {
|
|
t.Errorf("expected 3 dirty nodes, got %v", dirtyResp["nodes"])
|
|
}
|
|
}
|
|
|
|
func TestHandleGetDAG_NotFound(t *testing.T) {
|
|
s := newDAGTestServer(t)
|
|
r := newDAGRouter(s)
|
|
|
|
req := httptest.NewRequest("GET", "/api/items/NONEXISTENT-999/dag", nil)
|
|
w := httptest.NewRecorder()
|
|
r.ServeHTTP(w, req)
|
|
|
|
if w.Code != http.StatusNotFound {
|
|
t.Errorf("expected 404, got %d", w.Code)
|
|
}
|
|
}
|