Migration 007 introduces the v5 routing primitive: - clamps table (project-scoped, optional frame_id, excalidraw_id). - cable_clamps join (cable_id, clamp_id, ord) with PK on (cable_id, ord) and UNIQUE (cable_id, clamp_id) to block a clamp visiting the same cable twice. Store helpers in internal/db/clamps.go: - CreateClamp / GetClamp / ListClamps / UpdateClamp / DeleteClamp — standard project-scoped CRUD. UpdateClamp uses FrameRef tri-state. - AttachClampToCable — appends or inserts at a given ord. Mid-sequence inserts use a two-pass shift (bump by 10000, settle to ord+1) since SQLite UPDATE doesn't support ORDER BY and a single bulk +1 would collide with the UNIQUE (cable_id, ord) PK. - DetachClampFromCable — removes the row then closes the gap. - ReorderCableClamps — replaces the whole sequence in one tx. - ListClampsForCable / ListCableClamps — read helpers. Snapshot now carries clamps + cable_clamps arrays so the frontend can hydrate everything in one call. Tests cover create / update / cascade-delete / attach (append + insert + duplicate-rejected) / detach (gap closes) / reorder / snapshot.
189 lines
7.3 KiB
Go
189 lines
7.3 KiB
Go
package db
|
|
|
|
import (
|
|
"errors"
|
|
"testing"
|
|
)
|
|
|
|
func TestCreateClamp_Basic(t *testing.T) {
|
|
s := newTestStore(t)
|
|
p, _ := s.CreateProject("LOFT", "", "")
|
|
c, err := s.CreateClamp(p.ID, ClampCreate{X: 100, Y: 200, Label: "trunk-1"})
|
|
if err != nil {
|
|
t.Fatalf("create: %v", err)
|
|
}
|
|
if c.X != 100 || c.Y != 200 || c.Label != "trunk-1" {
|
|
t.Errorf("bad shape: %+v", c)
|
|
}
|
|
if c.ProjectID != p.ID {
|
|
t.Errorf("project_id mismatch: got %d, want %d", c.ProjectID, p.ID)
|
|
}
|
|
}
|
|
|
|
func TestUpdateClamp_PositionAndLabel(t *testing.T) {
|
|
s := newTestStore(t)
|
|
p, _ := s.CreateProject("LOFT", "", "")
|
|
c, _ := s.CreateClamp(p.ID, ClampCreate{X: 0, Y: 0})
|
|
nx, ny := 50.0, 75.0
|
|
lbl := "renamed"
|
|
upd, err := s.UpdateClamp(p.ID, c.ID, ClampUpdate{X: &nx, Y: &ny, Label: &lbl})
|
|
if err != nil {
|
|
t.Fatalf("update: %v", err)
|
|
}
|
|
if upd.X != 50 || upd.Y != 75 || upd.Label != "renamed" {
|
|
t.Errorf("update didn't take: %+v", upd)
|
|
}
|
|
}
|
|
|
|
func TestDeleteClamp_CascadesToCableClamps(t *testing.T) {
|
|
s := newTestStore(t)
|
|
p, _ := s.CreateProject("LOFT", "", "")
|
|
pcT := builtInTypeID(t, s, "PC")
|
|
a, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "A", TypeID: &pcT, X: 0, Y: 0, Width: 100, Height: 35})
|
|
b, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "B", TypeID: &pcT, X: 200, Y: 0, Width: 100, Height: 35})
|
|
cab, _ := s.CreateCable(p.ID, CableCreate{TypeID: 1,
|
|
From: CableEndpoint{DeviceID: &a.ID}, To: CableEndpoint{DeviceID: &b.ID}})
|
|
cl, _ := s.CreateClamp(p.ID, ClampCreate{X: 100, Y: 50})
|
|
if _, err := s.AttachClampToCable(p.ID, cab.ID, cl.ID, 0); err != nil {
|
|
t.Fatalf("attach: %v", err)
|
|
}
|
|
if err := s.DeleteClamp(p.ID, cl.ID); err != nil {
|
|
t.Fatalf("delete: %v", err)
|
|
}
|
|
rows, _ := s.ListClampsForCable(p.ID, cab.ID)
|
|
if len(rows) != 0 {
|
|
t.Errorf("cable_clamps not cleared: %+v", rows)
|
|
}
|
|
}
|
|
|
|
func TestAttachClampToCable_AppendsAndOrders(t *testing.T) {
|
|
s := newTestStore(t)
|
|
p, _ := s.CreateProject("LOFT", "", "")
|
|
pcT := builtInTypeID(t, s, "PC")
|
|
a, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "A", TypeID: &pcT, X: 0, Y: 0, Width: 100, Height: 35})
|
|
b, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "B", TypeID: &pcT, X: 200, Y: 0, Width: 100, Height: 35})
|
|
cab, _ := s.CreateCable(p.ID, CableCreate{TypeID: 1,
|
|
From: CableEndpoint{DeviceID: &a.ID}, To: CableEndpoint{DeviceID: &b.ID}})
|
|
c1, _ := s.CreateClamp(p.ID, ClampCreate{X: 50, Y: 0})
|
|
c2, _ := s.CreateClamp(p.ID, ClampCreate{X: 100, Y: 0})
|
|
c3, _ := s.CreateClamp(p.ID, ClampCreate{X: 150, Y: 0})
|
|
cc1, _ := s.AttachClampToCable(p.ID, cab.ID, c1.ID, 0)
|
|
cc2, _ := s.AttachClampToCable(p.ID, cab.ID, c2.ID, 0)
|
|
cc3, _ := s.AttachClampToCable(p.ID, cab.ID, c3.ID, 0)
|
|
if cc1.Ord != 1 || cc2.Ord != 2 || cc3.Ord != 3 {
|
|
t.Errorf("ord sequence wrong: %d, %d, %d", cc1.Ord, cc2.Ord, cc3.Ord)
|
|
}
|
|
}
|
|
|
|
func TestAttachClampToCable_InsertShiftsExisting(t *testing.T) {
|
|
s := newTestStore(t)
|
|
p, _ := s.CreateProject("LOFT", "", "")
|
|
pcT := builtInTypeID(t, s, "PC")
|
|
a, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "A", TypeID: &pcT, X: 0, Y: 0, Width: 100, Height: 35})
|
|
b, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "B", TypeID: &pcT, X: 200, Y: 0, Width: 100, Height: 35})
|
|
cab, _ := s.CreateCable(p.ID, CableCreate{TypeID: 1,
|
|
From: CableEndpoint{DeviceID: &a.ID}, To: CableEndpoint{DeviceID: &b.ID}})
|
|
c1, _ := s.CreateClamp(p.ID, ClampCreate{X: 50, Y: 0})
|
|
c2, _ := s.CreateClamp(p.ID, ClampCreate{X: 100, Y: 0})
|
|
c3, _ := s.CreateClamp(p.ID, ClampCreate{X: 150, Y: 0})
|
|
_, _ = s.AttachClampToCable(p.ID, cab.ID, c1.ID, 0) // ord=1
|
|
_, _ = s.AttachClampToCable(p.ID, cab.ID, c2.ID, 0) // ord=2
|
|
// Insert c3 between c1 and c2 → c3 gets ord=2, old c2 bumps to 3.
|
|
if _, err := s.AttachClampToCable(p.ID, cab.ID, c3.ID, 2); err != nil {
|
|
t.Fatalf("attach mid: %v", err)
|
|
}
|
|
got, _ := s.ListClampsForCable(p.ID, cab.ID)
|
|
if len(got) != 3 {
|
|
t.Fatalf("len = %d, want 3: %+v", len(got), got)
|
|
}
|
|
want := []struct{ id int64; ord int }{
|
|
{c1.ID, 1}, {c3.ID, 2}, {c2.ID, 3},
|
|
}
|
|
for i, w := range want {
|
|
if got[i].ClampID != w.id || got[i].Ord != w.ord {
|
|
t.Errorf("[%d] got %+v, want clamp=%d ord=%d", i, got[i], w.id, w.ord)
|
|
}
|
|
}
|
|
}
|
|
|
|
func TestAttachClampToCable_DuplicateRejected(t *testing.T) {
|
|
s := newTestStore(t)
|
|
p, _ := s.CreateProject("LOFT", "", "")
|
|
pcT := builtInTypeID(t, s, "PC")
|
|
a, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "A", TypeID: &pcT, X: 0, Y: 0, Width: 100, Height: 35})
|
|
b, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "B", TypeID: &pcT, X: 200, Y: 0, Width: 100, Height: 35})
|
|
cab, _ := s.CreateCable(p.ID, CableCreate{TypeID: 1,
|
|
From: CableEndpoint{DeviceID: &a.ID}, To: CableEndpoint{DeviceID: &b.ID}})
|
|
c1, _ := s.CreateClamp(p.ID, ClampCreate{X: 50, Y: 0})
|
|
_, _ = s.AttachClampToCable(p.ID, cab.ID, c1.ID, 0)
|
|
if _, err := s.AttachClampToCable(p.ID, cab.ID, c1.ID, 0); !errors.Is(err, ErrConflict) {
|
|
t.Errorf("duplicate err = %v, want ErrConflict", err)
|
|
}
|
|
}
|
|
|
|
func TestDetachClampFromCable_ClosesGap(t *testing.T) {
|
|
s := newTestStore(t)
|
|
p, _ := s.CreateProject("LOFT", "", "")
|
|
pcT := builtInTypeID(t, s, "PC")
|
|
a, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "A", TypeID: &pcT, X: 0, Y: 0, Width: 100, Height: 35})
|
|
b, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "B", TypeID: &pcT, X: 200, Y: 0, Width: 100, Height: 35})
|
|
cab, _ := s.CreateCable(p.ID, CableCreate{TypeID: 1,
|
|
From: CableEndpoint{DeviceID: &a.ID}, To: CableEndpoint{DeviceID: &b.ID}})
|
|
c1, _ := s.CreateClamp(p.ID, ClampCreate{X: 50, Y: 0})
|
|
c2, _ := s.CreateClamp(p.ID, ClampCreate{X: 100, Y: 0})
|
|
c3, _ := s.CreateClamp(p.ID, ClampCreate{X: 150, Y: 0})
|
|
_, _ = s.AttachClampToCable(p.ID, cab.ID, c1.ID, 0)
|
|
_, _ = s.AttachClampToCable(p.ID, cab.ID, c2.ID, 0)
|
|
_, _ = s.AttachClampToCable(p.ID, cab.ID, c3.ID, 0)
|
|
if err := s.DetachClampFromCable(p.ID, cab.ID, c2.ID); err != nil {
|
|
t.Fatalf("detach: %v", err)
|
|
}
|
|
got, _ := s.ListClampsForCable(p.ID, cab.ID)
|
|
if len(got) != 2 {
|
|
t.Fatalf("len = %d, want 2", len(got))
|
|
}
|
|
if got[0].ClampID != c1.ID || got[0].Ord != 1 {
|
|
t.Errorf("[0] = %+v", got[0])
|
|
}
|
|
if got[1].ClampID != c3.ID || got[1].Ord != 2 {
|
|
t.Errorf("[1] = %+v", got[1])
|
|
}
|
|
}
|
|
|
|
func TestReorderCableClamps_FullReplace(t *testing.T) {
|
|
s := newTestStore(t)
|
|
p, _ := s.CreateProject("LOFT", "", "")
|
|
pcT := builtInTypeID(t, s, "PC")
|
|
a, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "A", TypeID: &pcT, X: 0, Y: 0, Width: 100, Height: 35})
|
|
b, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "B", TypeID: &pcT, X: 200, Y: 0, Width: 100, Height: 35})
|
|
cab, _ := s.CreateCable(p.ID, CableCreate{TypeID: 1,
|
|
From: CableEndpoint{DeviceID: &a.ID}, To: CableEndpoint{DeviceID: &b.ID}})
|
|
c1, _ := s.CreateClamp(p.ID, ClampCreate{X: 50, Y: 0})
|
|
c2, _ := s.CreateClamp(p.ID, ClampCreate{X: 100, Y: 0})
|
|
c3, _ := s.CreateClamp(p.ID, ClampCreate{X: 150, Y: 0})
|
|
if _, err := s.ReorderCableClamps(p.ID, cab.ID, []int64{c3.ID, c1.ID, c2.ID}); err != nil {
|
|
t.Fatalf("reorder: %v", err)
|
|
}
|
|
got, _ := s.ListClampsForCable(p.ID, cab.ID)
|
|
if len(got) != 3 {
|
|
t.Fatalf("len = %d, want 3", len(got))
|
|
}
|
|
if got[0].ClampID != c3.ID || got[1].ClampID != c1.ID || got[2].ClampID != c2.ID {
|
|
t.Errorf("order wrong: %+v", got)
|
|
}
|
|
}
|
|
|
|
func TestSnapshot_IncludesClamps(t *testing.T) {
|
|
s := newTestStore(t)
|
|
p, _ := s.CreateProject("LOFT", "", "")
|
|
_, _ = s.CreateClamp(p.ID, ClampCreate{X: 10, Y: 20})
|
|
_, _ = s.CreateClamp(p.ID, ClampCreate{X: 30, Y: 40})
|
|
snap, err := s.Snapshot(p.ID)
|
|
if err != nil {
|
|
t.Fatalf("snapshot: %v", err)
|
|
}
|
|
if len(snap.Clamps) != 2 {
|
|
t.Errorf("clamps in snapshot = %d, want 2", len(snap.Clamps))
|
|
}
|
|
}
|