package db import ( "testing" ) // builtInTypeID returns the id of the named built-in device type. func builtInTypeID(t *testing.T, s *Store, name string) int64 { t.Helper() all, _ := s.ListBuiltInDeviceTypes() for _, dt := range all { if dt.Name == name { return dt.ID } } t.Fatalf("built-in %q not found", name) return 0 } // ------------------------------------------------------ basic solver wins func TestSolve_BasicNAStoSwitch(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") nasT := builtInTypeID(t, s, "NAS") swT := builtInTypeID(t, s, "Switch") nas, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "NAS", TypeID: &nasT, X: 0, Y: 0, Width: 100, Height: 35}) sw, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "Switch", TypeID: &swT, X: 200, Y: 0, Width: 100, Height: 35}) rj45 := int64(5) _, _ = s.CreateConnectionRequirement(p.ID, ConnectionRequirementCreate{ FromDeviceID: nas.ID, ToDeviceID: sw.ID, PreferredCableTypeID: &rj45, }) res, err := s.Solve(p.ID, false) if err != nil { t.Fatalf("solve: %v", err) } if len(res.CablesAdded) != 1 { t.Fatalf("cables_added len = %d, want 1", len(res.CablesAdded)) } if res.CablesAdded[0].TypeID != rj45 { t.Errorf("cable type = %d, want %d (RJ45)", res.CablesAdded[0].TypeID, rj45) } if !res.CablesAdded[0].Auto { t.Errorf("cable.auto should be true") } if len(res.Unsatisfied) != 0 { t.Errorf("unsatisfied should be empty; got %+v", res.Unsatisfied) } } func TestSolve_AmbiguousType_RequirementUnsatisfied(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") // Both PCs have Power + USB + HDMI + RJ45 → multiple types match. 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}) _, _ = s.CreateConnectionRequirement(p.ID, ConnectionRequirementCreate{ FromDeviceID: a.ID, ToDeviceID: b.ID, // no PreferredCableTypeID }) res, _ := s.Solve(p.ID, true) if len(res.CablesAdded) != 0 { t.Errorf("ambiguous: should not add cables, got %d", len(res.CablesAdded)) } if len(res.Unsatisfied) != 1 || res.Unsatisfied[0].Reason == "" { t.Errorf("expected 1 unsatisfied req with non-empty reason; got %+v", res.Unsatisfied) } } func TestSolve_NoFreePort_RequirementUnsatisfied(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") // Mouse only has 1 USB port. Two USB requirements against it should // leave one unsatisfied. mouseT := builtInTypeID(t, s, "Mouse") pcT := builtInTypeID(t, s, "PC") mouse, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "Mouse", TypeID: &mouseT, X: 0, Y: 0, Width: 100, Height: 35}) pc1, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "PC1", TypeID: &pcT, X: 200, Y: 0, Width: 100, Height: 35}) pc2, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "PC2", TypeID: &pcT, X: 400, Y: 0, Width: 100, Height: 35}) usb := int64(2) _, _ = s.CreateConnectionRequirement(p.ID, ConnectionRequirementCreate{ FromDeviceID: mouse.ID, ToDeviceID: pc1.ID, PreferredCableTypeID: &usb, }) _, _ = s.CreateConnectionRequirement(p.ID, ConnectionRequirementCreate{ FromDeviceID: mouse.ID, ToDeviceID: pc2.ID, PreferredCableTypeID: &usb, }) res, _ := s.Solve(p.ID, true) if len(res.CablesAdded) != 1 { t.Errorf("expected 1 cable to land (one mouse USB), got %d", len(res.CablesAdded)) } if len(res.Unsatisfied) != 1 { t.Errorf("expected 1 unsatisfied; got %d (%+v)", len(res.Unsatisfied), res.Unsatisfied) } } // ----------------------------------------------- preview vs apply semantics func TestSolve_PreviewDoesNotWrite(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") nasT := builtInTypeID(t, s, "NAS") swT := builtInTypeID(t, s, "Switch") nas, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "NAS", TypeID: &nasT, X: 0, Y: 0, Width: 100, Height: 35}) sw, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "Switch", TypeID: &swT, X: 200, Y: 0, Width: 100, Height: 35}) rj45 := int64(5) _, _ = s.CreateConnectionRequirement(p.ID, ConnectionRequirementCreate{ FromDeviceID: nas.ID, ToDeviceID: sw.ID, PreferredCableTypeID: &rj45, }) _, _ = s.Solve(p.ID, true) // preview cables, _ := s.ListCables(p.ID) if len(cables) != 0 { t.Errorf("preview wrote %d cables; want 0", len(cables)) } } func TestSolve_ApplyThenIdempotent(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") nasT := builtInTypeID(t, s, "NAS") swT := builtInTypeID(t, s, "Switch") nas, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "NAS", TypeID: &nasT, X: 0, Y: 0, Width: 100, Height: 35}) sw, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "Switch", TypeID: &swT, X: 200, Y: 0, Width: 100, Height: 35}) rj45 := int64(5) _, _ = s.CreateConnectionRequirement(p.ID, ConnectionRequirementCreate{ FromDeviceID: nas.ID, ToDeviceID: sw.ID, PreferredCableTypeID: &rj45, }) r1, _ := s.Solve(p.ID, false) if len(r1.CablesAdded) != 1 { t.Fatalf("first apply: cables_added=%d, want 1", len(r1.CablesAdded)) } r2, _ := s.Solve(p.ID, false) if len(r2.CablesAdded) != 0 { t.Errorf("second apply: cables_added=%d, want 0 (idempotent)", len(r2.CablesAdded)) } if len(r2.CablesKept) != 1 { t.Errorf("second apply: cables_kept=%d, want 1", len(r2.CablesKept)) } } func TestSolve_ManualCableReservesPort(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") mouseT := builtInTypeID(t, s, "Mouse") pcT := builtInTypeID(t, s, "PC") mouse, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "Mouse", TypeID: &mouseT, X: 0, Y: 0, Width: 100, Height: 35}) pc, _ := s.CreateDevice(p.ID, DeviceCreate{Name: "PC", TypeID: &pcT, X: 200, Y: 0, Width: 100, Height: 35}) // Manual cable USB Mouse↔PC: claims the only mouse USB port. ports, _ := s.ListPortsForProject(p.ID) var mouseUSB, pcUSB int64 for _, prt := range ports { if prt.DeviceID == mouse.ID && prt.TypeID == 2 { mouseUSB = prt.ID } if prt.DeviceID == pc.ID && prt.TypeID == 2 { pcUSB = prt.ID break } } usb := int64(2) _, _ = s.CreateCable(p.ID, CableCreate{ TypeID: usb, From: CableEndpoint{PortID: &mouseUSB}, To: CableEndpoint{PortID: &pcUSB}, Auto: false, }) // Now add a requirement that also wants USB on the mouse → no free port. _, _ = s.CreateConnectionRequirement(p.ID, ConnectionRequirementCreate{ FromDeviceID: mouse.ID, ToDeviceID: pc.ID, PreferredCableTypeID: &usb, }) res, _ := s.Solve(p.ID, true) if len(res.Unsatisfied) == 0 { t.Errorf("expected unsatisfied req (manual cable should reserve the only mouse USB port)") } } // -------------------------------------------------------- setup templates func TestApplyTemplate_LivingRoom(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") tmpls, _ := s.ListSetupTemplates() var lr SetupTemplate for _, tm := range tmpls { if tm.Name == "Living Room" { lr = tm break } } if lr.ID == 0 { t.Fatal("Living Room template not seeded") } res, err := s.ApplyTemplate(p.ID, lr.ID, ApplyTemplateOptions{}) if err != nil { t.Fatalf("apply: %v", err) } if len(res.DevicesAdded) != 3 { t.Errorf("devices added = %d, want 3 (TV, Soundbar, ChromeCast)", len(res.DevicesAdded)) } if len(res.RequirementsAdded) != 2 { t.Errorf("requirements added = %d, want 2 (TV↔Soundbar, TV↔ChromeCast)", len(res.RequirementsAdded)) } // Ports were seeded as part of the device creation. ports, _ := s.ListPortsForProject(p.ID) if len(ports) < 6 { // TV(3) + Soundbar(2) + ChromeCast(2) = 7 t.Errorf("ports after template apply = %d, expected ≥6", len(ports)) } } func TestApplyTemplate_HomeOffice_ThenSolve(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") tmpls, _ := s.ListSetupTemplates() var ho SetupTemplate for _, tm := range tmpls { if tm.Name == "Home Office" { ho = tm break } } if _, err := s.ApplyTemplate(p.ID, ho.ID, ApplyTemplateOptions{}); err != nil { t.Fatalf("apply: %v", err) } res, err := s.Solve(p.ID, false) if err != nil { t.Fatalf("solve: %v", err) } if len(res.CablesAdded) != 3 { t.Errorf("Home Office should solve to 3 cables (PC↔Screen, PC↔Keyboard, PC↔Mouse); got %d", len(res.CablesAdded)) } if len(res.Unsatisfied) != 0 { t.Errorf("unsatisfied = %+v, want []", res.Unsatisfied) } } func TestApplyTemplate_NameCollisionSkipped(t *testing.T) { s := newTestStore(t) p, _ := s.CreateProject("LOFT", "", "") pcT := builtInTypeID(t, s, "PC") // Pre-create a device called "PC" so the Home Office template's PC collides. _, _ = s.CreateDevice(p.ID, DeviceCreate{Name: "PC", TypeID: &pcT, X: 0, Y: 0, Width: 100, Height: 35}) tmpls, _ := s.ListSetupTemplates() var ho SetupTemplate for _, tm := range tmpls { if tm.Name == "Home Office" { ho = tm break } } res, _ := s.ApplyTemplate(p.ID, ho.ID, ApplyTemplateOptions{}) if len(res.SkippedDevices) == 0 { t.Errorf("expected at least one skipped device for name collision; got %+v", res.SkippedDevices) } if len(res.RequirementsSkipped) == 0 { t.Errorf("PC requirements should be skipped when PC device skipped; got %+v", res.RequirementsSkipped) } }