Files
silo/internal/api/dag_handlers_test.go

248 lines
6.8 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/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, "",
)
}
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)
}
}